package common

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	opv1alpha1 "github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
)

func getTestComponentSpec(version string) opv1alpha1.ComponentSpec {
	return opv1alpha1.ComponentSpec{
		Annotations: map[string]string{
			RestartTiDBAnnotation: version,
		},
	}
}

func Test_mutateTCForRestarting(t *testing.T) {
	tests := []struct {
		name        string
		tc          *opv1alpha1.TidbCluster
		version     string
		wantChanged bool
	}{
		{
			name: "the annotation is not set",
			tc: &opv1alpha1.TidbCluster{
				Spec: opv1alpha1.TidbClusterSpec{
					PD:      &opv1alpha1.PDSpec{},
					TiKV:    &opv1alpha1.TiKVSpec{},
					TiDB:    &opv1alpha1.TiDBSpec{},
					TiFlash: &opv1alpha1.TiFlashSpec{},
				},
			},
			version:     "1.24",
			wantChanged: true,
		},
		{
			name: "the annotation is already set",
			tc: &opv1alpha1.TidbCluster{
				Spec: opv1alpha1.TidbClusterSpec{
					PD: &opv1alpha1.PDSpec{
						ComponentSpec: getTestComponentSpec("1.24"),
					},
					TiKV: &opv1alpha1.TiKVSpec{
						ComponentSpec: getTestComponentSpec("1.24"),
					},
					TiDB: &opv1alpha1.TiDBSpec{
						ComponentSpec: getTestComponentSpec("1.24"),
					},
					TiFlash: &opv1alpha1.TiFlashSpec{
						ComponentSpec: getTestComponentSpec("1.24"),
					},
				},
			},
			version:     "1.24",
			wantChanged: false,
		},
		{
			name: "the annotation value is different from the desired version",
			tc: &opv1alpha1.TidbCluster{
				Spec: opv1alpha1.TidbClusterSpec{
					PD: &opv1alpha1.PDSpec{
						ComponentSpec: getTestComponentSpec("1.24"),
					},
					TiKV: &opv1alpha1.TiKVSpec{
						ComponentSpec: getTestComponentSpec("1.24"),
					},
					TiDB: &opv1alpha1.TiDBSpec{
						ComponentSpec: getTestComponentSpec("1.24"),
					},
					TiFlash: &opv1alpha1.TiFlashSpec{
						ComponentSpec: getTestComponentSpec("1.24"),
					},
				},
			},
			version:     "1.25",
			wantChanged: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if gotChanged := MutateTCForRestarting(tt.tc, tt.version); gotChanged != tt.wantChanged {
				t.Errorf("MutateTCForRestarting() = %v, want %v", gotChanged, tt.wantChanged)
			}
		})
	}
}

func TestMutateTCForRestartingTiProxyOnly(t *testing.T) {
	tc := &opv1alpha1.TidbCluster{
		Spec: opv1alpha1.TidbClusterSpec{
			PD:      &opv1alpha1.PDSpec{},
			TiKV:    &opv1alpha1.TiKVSpec{},
			TiDB:    &opv1alpha1.TiDBSpec{},
			TiFlash: &opv1alpha1.TiFlashSpec{},
			TiProxy: &opv1alpha1.TiProxySpec{},
		},
	}

	changed := MutateTCForRestartingTiProxyOnly(tc, "1.30")
	if !changed {
		t.Fatalf("expected changed to be true")
	}
	if tc.Spec.TiProxy.ComponentSpec.Annotations[RestartTiDBAnnotation] != "1.30" {
		t.Fatalf("expected tiproxy annotation to be set")
	}
	if tc.Spec.PD.ComponentSpec.Annotations != nil {
		t.Fatalf("expected pd annotation unchanged")
	}
	if tc.Spec.TiKV.ComponentSpec.Annotations != nil {
		t.Fatalf("expected tikv annotation unchanged")
	}
	if tc.Spec.TiDB.ComponentSpec.Annotations != nil {
		t.Fatalf("expected tidb annotation unchanged")
	}
	if tc.Spec.TiFlash.ComponentSpec.Annotations != nil {
		t.Fatalf("expected tiflash annotation unchanged")
	}
}

func TestMutateTCForRestartingWithoutTiProxy(t *testing.T) {
	tc := &opv1alpha1.TidbCluster{
		Spec: opv1alpha1.TidbClusterSpec{
			PD:      &opv1alpha1.PDSpec{},
			TiKV:    &opv1alpha1.TiKVSpec{},
			TiDB:    &opv1alpha1.TiDBSpec{},
			TiFlash: &opv1alpha1.TiFlashSpec{},
			TiProxy: &opv1alpha1.TiProxySpec{},
		},
	}

	changed := MutateTCForRestartingWithoutTiProxy(tc, "1.30")
	if !changed {
		t.Fatalf("expected changed to be true")
	}
	if tc.Spec.PD.ComponentSpec.Annotations[RestartTiDBAnnotation] != "1.30" {
		t.Fatalf("expected pd annotation to be set")
	}
	if tc.Spec.TiKV.ComponentSpec.Annotations[RestartTiDBAnnotation] != "1.30" {
		t.Fatalf("expected tikv annotation to be set")
	}
	if tc.Spec.TiDB.ComponentSpec.Annotations[RestartTiDBAnnotation] != "1.30" {
		t.Fatalf("expected tidb annotation to be set")
	}
	if tc.Spec.TiFlash.ComponentSpec.Annotations[RestartTiDBAnnotation] != "1.30" {
		t.Fatalf("expected tiflash annotation to be set")
	}
	if tc.Spec.TiProxy.ComponentSpec.Annotations != nil {
		t.Fatalf("expected tiproxy annotation unchanged")
	}
}

func TestMutateTCForRestartingComponents(t *testing.T) {
	const version = "1.30"

	newTCAllComponents := func() *opv1alpha1.TidbCluster {
		return &opv1alpha1.TidbCluster{
			Spec: opv1alpha1.TidbClusterSpec{
				PD:      &opv1alpha1.PDSpec{},
				TiKV:    &opv1alpha1.TiKVSpec{},
				TiDB:    &opv1alpha1.TiDBSpec{},
				TiFlash: &opv1alpha1.TiFlashSpec{},
				TiProxy: &opv1alpha1.TiProxySpec{},
			},
		}
	}

	tests := []struct {
		name          string
		components    []cloudv1.ComponentType
		wantChanged   bool
		wantAnnotated []cloudv1.ComponentType
		wantUnchanged []cloudv1.ComponentType
	}{
		{
			name:          "TiProxy only",
			components:    []cloudv1.ComponentType{cloudv1.ComponentTiProxy},
			wantChanged:   true,
			wantAnnotated: []cloudv1.ComponentType{cloudv1.ComponentTiProxy},
			wantUnchanged: []cloudv1.ComponentType{cloudv1.ComponentPD, cloudv1.ComponentTiKV, cloudv1.ComponentTiDB, cloudv1.ComponentTiFlash},
		},
		{
			name:          "Storage (PD/TiKV/TiFlash)",
			components:    []cloudv1.ComponentType{cloudv1.ComponentPD, cloudv1.ComponentTiKV, cloudv1.ComponentTiFlash},
			wantChanged:   true,
			wantAnnotated: []cloudv1.ComponentType{cloudv1.ComponentPD, cloudv1.ComponentTiKV, cloudv1.ComponentTiFlash},
			wantUnchanged: []cloudv1.ComponentType{cloudv1.ComponentTiDB, cloudv1.ComponentTiProxy},
		},
		{
			name:          "TiDB only",
			components:    []cloudv1.ComponentType{cloudv1.ComponentTiDB},
			wantChanged:   true,
			wantAnnotated: []cloudv1.ComponentType{cloudv1.ComponentTiDB},
			wantUnchanged: []cloudv1.ComponentType{cloudv1.ComponentPD, cloudv1.ComponentTiKV, cloudv1.ComponentTiFlash, cloudv1.ComponentTiProxy},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tc := newTCAllComponents()
			changed := MutateTCForRestartingComponents(tc, version, tt.components...)
			if changed != tt.wantChanged {
				t.Fatalf("changed=%v, want %v", changed, tt.wantChanged)
			}
			for _, comp := range tt.wantAnnotated {
				got := getAnnotationForComponent(tc, comp)
				if got != version {
					t.Errorf("component %s annotation=%q, want %q", comp, got, version)
				}
			}
			for _, comp := range tt.wantUnchanged {
				got := getAnnotationForComponent(tc, comp)
				if got != "" {
					t.Errorf("component %s should be unchanged but annotation=%q", comp, got)
				}
			}
		})
	}

	t.Run("idempotent: already set returns changed=false", func(t *testing.T) {
		tc := newTCAllComponents()
		_ = MutateTCForRestartingComponents(tc, version, cloudv1.ComponentPD, cloudv1.ComponentTiKV, cloudv1.ComponentTiFlash)
		if changed := MutateTCForRestartingComponents(tc, version, cloudv1.ComponentPD, cloudv1.ComponentTiKV, cloudv1.ComponentTiFlash); changed {
			t.Fatalf("second call should return changed=false")
		}
	})

	t.Run("nil spec component is skipped silently", func(t *testing.T) {
		tc := &opv1alpha1.TidbCluster{
			Spec: opv1alpha1.TidbClusterSpec{
				PD: &opv1alpha1.PDSpec{},
				// TiProxy spec is nil
			},
		}
		if changed := MutateTCForRestartingComponents(tc, version, cloudv1.ComponentTiProxy, cloudv1.ComponentPD); !changed {
			t.Fatalf("PD annotation should have changed")
		}
		if tc.Spec.PD.ComponentSpec.Annotations[RestartTiDBAnnotation] != version {
			t.Fatalf("PD annotation not set")
		}
	})
}

// getAnnotationForComponent is a test helper that returns the restart annotation value for a component.
func getAnnotationForComponent(tc *opv1alpha1.TidbCluster, comp cloudv1.ComponentType) string {
	switch comp {
	case cloudv1.ComponentTiProxy:
		if tc.Spec.TiProxy != nil {
			return tc.Spec.TiProxy.ComponentSpec.Annotations[RestartTiDBAnnotation]
		}
	case cloudv1.ComponentPD:
		if tc.Spec.PD != nil {
			return tc.Spec.PD.ComponentSpec.Annotations[RestartTiDBAnnotation]
		}
	case cloudv1.ComponentTiKV:
		if tc.Spec.TiKV != nil {
			return tc.Spec.TiKV.ComponentSpec.Annotations[RestartTiDBAnnotation]
		}
	case cloudv1.ComponentTiFlash:
		if tc.Spec.TiFlash != nil {
			return tc.Spec.TiFlash.ComponentSpec.Annotations[RestartTiDBAnnotation]
		}
	case cloudv1.ComponentTiDB:
		if tc.Spec.TiDB != nil {
			return tc.Spec.TiDB.ComponentSpec.Annotations[RestartTiDBAnnotation]
		}
	}
	return ""
}

func TestIsComponentRestartedAndReady(t *testing.T) {
	const version = "1.30"

	annotated := func() opv1alpha1.ComponentSpec {
		return opv1alpha1.ComponentSpec{
			Annotations: map[string]string{RestartTiDBAnnotation: version},
		}
	}

	tests := []struct {
		name      string
		component cloudv1.ComponentType
		tc        *opv1alpha1.TidbCluster
		want      bool
	}{
		// nil TC
		{name: "nil TC", component: cloudv1.ComponentPD, tc: nil, want: true},

		// PD cases
		{
			name:      "PD nil spec → true",
			component: cloudv1.ComponentPD,
			tc:        &opv1alpha1.TidbCluster{},
			want:      true,
		},
		{
			name:      "PD replicas=0 → true",
			component: cloudv1.ComponentPD,
			tc: &opv1alpha1.TidbCluster{
				Spec: opv1alpha1.TidbClusterSpec{PD: &opv1alpha1.PDSpec{Replicas: 0}},
			},
			want: true,
		},
		{
			name:      "PD annotation mismatch → false",
			component: cloudv1.ComponentPD,
			tc: &opv1alpha1.TidbCluster{
				Spec: opv1alpha1.TidbClusterSpec{
					PD: &opv1alpha1.PDSpec{Replicas: 1},
				},
			},
			want: false,
		},
		{
			name:      "PD upgrading → false",
			component: cloudv1.ComponentPD,
			tc: &opv1alpha1.TidbCluster{
				Spec: opv1alpha1.TidbClusterSpec{
					PD: &opv1alpha1.PDSpec{Replicas: 1, ComponentSpec: annotated()},
				},
				Status: opv1alpha1.TidbClusterStatus{
					PD: opv1alpha1.PDStatus{Synced: true, Phase: opv1alpha1.UpgradePhase,
						Members: map[string]opv1alpha1.PDMember{"pd-0": {Name: "pd-0", Health: true}}},
				},
			},
			want: false,
		},
		{
			name:      "PD not synced → false",
			component: cloudv1.ComponentPD,
			tc: &opv1alpha1.TidbCluster{
				Spec: opv1alpha1.TidbClusterSpec{
					PD: &opv1alpha1.PDSpec{Replicas: 1, ComponentSpec: annotated()},
				},
				Status: opv1alpha1.TidbClusterStatus{
					PD: opv1alpha1.PDStatus{Synced: false, Phase: opv1alpha1.NormalPhase,
						Members: map[string]opv1alpha1.PDMember{"pd-0": {Name: "pd-0", Health: true}}},
				},
			},
			want: false,
		},
		{
			name:      "PD ready → true",
			component: cloudv1.ComponentPD,
			tc: &opv1alpha1.TidbCluster{
				Spec: opv1alpha1.TidbClusterSpec{
					PD: &opv1alpha1.PDSpec{Replicas: 1, ComponentSpec: annotated()},
				},
				Status: opv1alpha1.TidbClusterStatus{
					PD: opv1alpha1.PDStatus{Synced: true, Phase: opv1alpha1.NormalPhase,
						Members: map[string]opv1alpha1.PDMember{"pd-0": {Name: "pd-0", Health: true}}},
				},
			},
			want: true,
		},

		// TiDB cases (no Synced field)
		{
			name:      "TiDB nil spec → true",
			component: cloudv1.ComponentTiDB,
			tc:        &opv1alpha1.TidbCluster{},
			want:      true,
		},
		{
			name:      "TiDB replicas=0 → true",
			component: cloudv1.ComponentTiDB,
			tc: &opv1alpha1.TidbCluster{
				Spec: opv1alpha1.TidbClusterSpec{TiDB: &opv1alpha1.TiDBSpec{Replicas: 0}},
			},
			want: true,
		},
		{
			name:      "TiDB phase not normal → false",
			component: cloudv1.ComponentTiDB,
			tc: &opv1alpha1.TidbCluster{
				Spec: opv1alpha1.TidbClusterSpec{
					TiDB: &opv1alpha1.TiDBSpec{Replicas: 1, ComponentSpec: annotated()},
				},
				Status: opv1alpha1.TidbClusterStatus{
					TiDB: opv1alpha1.TiDBStatus{Phase: opv1alpha1.UpgradePhase,
						Members: map[string]opv1alpha1.TiDBMember{"tidb-0": {Health: true}}},
				},
			},
			want: false,
		},
		{
			name:      "TiDB not all members ready → false",
			component: cloudv1.ComponentTiDB,
			tc: &opv1alpha1.TidbCluster{
				Spec: opv1alpha1.TidbClusterSpec{
					TiDB: &opv1alpha1.TiDBSpec{Replicas: 1, ComponentSpec: annotated()},
				},
				Status: opv1alpha1.TidbClusterStatus{
					TiDB: opv1alpha1.TiDBStatus{Phase: opv1alpha1.NormalPhase,
						Members: map[string]opv1alpha1.TiDBMember{"tidb-0": {Health: false}}},
				},
			},
			want: false,
		},
		{
			name:      "TiDB ready → true",
			component: cloudv1.ComponentTiDB,
			tc: &opv1alpha1.TidbCluster{
				Spec: opv1alpha1.TidbClusterSpec{
					TiDB: &opv1alpha1.TiDBSpec{Replicas: 1, ComponentSpec: annotated()},
				},
				Status: opv1alpha1.TidbClusterStatus{
					TiDB: opv1alpha1.TiDBStatus{Phase: opv1alpha1.NormalPhase,
						Members: map[string]opv1alpha1.TiDBMember{"tidb-0": {Health: true}}},
				},
			},
			want: true,
		},
	}

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

func TestIsTiProxyRestartedAndReady(t *testing.T) {
	tests := []struct {
		name string
		tc   *opv1alpha1.TidbCluster
		want bool
	}{
		{
			name: "no tiproxy configured",
			tc:   &opv1alpha1.TidbCluster{},
			want: true,
		},
		{
			name: "tiproxy annotation mismatch",
			tc: &opv1alpha1.TidbCluster{
				Spec: opv1alpha1.TidbClusterSpec{
					TiProxy: &opv1alpha1.TiProxySpec{
						Replicas:      1,
						ComponentSpec: getTestComponentSpec("1.29"),
					},
				},
				Status: opv1alpha1.TidbClusterStatus{
					TiProxy: opv1alpha1.TiProxyStatus{
						Synced: true,
						Phase:  opv1alpha1.NormalPhase,
						Members: map[string]opv1alpha1.TiProxyMember{
							"tiproxy-0": {Health: true},
						},
					},
				},
			},
			want: false,
		},
		{
			name: "tiproxy still upgrading",
			tc: &opv1alpha1.TidbCluster{
				Spec: opv1alpha1.TidbClusterSpec{
					TiProxy: &opv1alpha1.TiProxySpec{
						Replicas:      1,
						ComponentSpec: getTestComponentSpec("1.30"),
					},
				},
				Status: opv1alpha1.TidbClusterStatus{
					TiProxy: opv1alpha1.TiProxyStatus{
						Synced: true,
						Phase:  opv1alpha1.UpgradePhase,
						Members: map[string]opv1alpha1.TiProxyMember{
							"tiproxy-0": {Health: true},
						},
					},
				},
			},
			want: false,
		},
		{
			name: "tiproxy synced and healthy",
			tc: &opv1alpha1.TidbCluster{
				Spec: opv1alpha1.TidbClusterSpec{
					TiProxy: &opv1alpha1.TiProxySpec{
						Replicas:      1,
						ComponentSpec: getTestComponentSpec("1.30"),
					},
				},
				Status: opv1alpha1.TidbClusterStatus{
					TiProxy: opv1alpha1.TiProxyStatus{
						Synced: true,
						Phase:  opv1alpha1.NormalPhase,
						Members: map[string]opv1alpha1.TiProxyMember{
							"tiproxy-0": {Health: true},
						},
					},
				},
			},
			want: true,
		},
	}

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

const (
	testClusterID = "123456789"
)

func TestRestartTiDBCluster(t *testing.T) {
	tests := []struct {
		name          string
		cluster       *cloudv1.Cluster
		version       string
		setupServer   func(t *testing.T) *httptest.Server
		expectedError bool
	}{
		{
			name:    "nil cluster",
			cluster: nil,
			version: "1.29",
			setupServer: func(t *testing.T) *httptest.Server {
				return nil
			},
			expectedError: true,
		},
		{
			name:    "nil gateway",
			cluster: &cloudv1.Cluster{},
			version: "1.29",
			setupServer: func(t *testing.T) *httptest.Server {
				return nil
			},
			expectedError: true,
		},
		{
			name: "successful restart",
			cluster: &cloudv1.Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Name: testClusterID,
				},
				Status: cloudv1.ClusterStatus{Gateway: &cloudv1.GatewayStatus{}},
			},
			version: "1.29",
			setupServer: func(t *testing.T) *httptest.Server {
				type req struct {
					ClusterID   string `json:"cluster_id"`
					OperationID string `json:"operation_id"`
					TargetType  string `json:"target_type"`
				}

				return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
					if r.URL.Path != fmt.Sprintf("/cluster-agent/tidb%s/api/v1/clusters/%s:restart", testClusterID, testClusterID) {
						t.Errorf("Expected to request '/cluster-agent', got: %s", r.URL.Path)
					}

					var body req
					if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
						t.Errorf("Failed to decode request body: %v", err)
					}
					if body.OperationID != "1.29" {
						t.Errorf("Expected operation ID to be '1.29', got: %s", body.OperationID)
					}
					if body.ClusterID != testClusterID {
						t.Errorf("Expected cluster ID to be %s, got: %s", testClusterID, body.ClusterID)
					}
					if body.TargetType != "CLUSTER" {
						t.Errorf("Expected target type to be 'CLUSTER', got: %s", body.TargetType)
					}

					w.WriteHeader(http.StatusOK)
				}))
			},
			expectedError: false,
		},
		{
			name: "server returns error",
			cluster: &cloudv1.Cluster{
				Status: cloudv1.ClusterStatus{Gateway: &cloudv1.GatewayStatus{}},
			},
			version: "1.24",
			setupServer: func(t *testing.T) *httptest.Server {
				return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
					w.WriteHeader(http.StatusInternalServerError)
				}))
			},
			expectedError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			server := tt.setupServer(t)
			if server != nil {
				defer server.Close()
				tt.cluster.Status.Gateway.Endpoint = server.URL
			}

			client := &http.Client{}
			err := RestartTiDBCluster(tt.cluster, client, tt.version)

			if (err != nil) != tt.expectedError {
				t.Errorf("RestartTiDBCluster() error = %v, expectedError %v", err, tt.expectedError)
			}
		})
	}
}

func TestPauseCluster(t *testing.T) {
	tests := []struct {
		name        string
		cluster     *cloudv1.Cluster
		wantUpdated bool
		wantErr     bool
	}{
		{
			name:        "nil cluster",
			cluster:     nil,
			wantUpdated: false,
			wantErr:     true,
		},
		{
			name: "cluster not in running or suspend phase",
			cluster: &cloudv1.Cluster{
				Status: cloudv1.ClusterStatus{
					Phase: cloudv1.ClusterPhaseCreating,
				},
			},
			wantUpdated: false,
			wantErr:     true,
		},
		{
			name: "successfully paused",
			cluster: &cloudv1.Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Name: testClusterID,
				},
				Status: cloudv1.ClusterStatus{
					Phase: cloudv1.ClusterPhaseRunning,
				},
			},
			wantUpdated: true,
			wantErr:     false,
		},
		{
			name: "already paused",
			cluster: &cloudv1.Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Annotations: map[string]string{
						cloudv1.AnnoPauseReconciliationReason: PauseReason,
					},
				},
				Spec: cloudv1.ClusterSpec{
					Paused: pointer.Bool(true),
				},
				Status: cloudv1.ClusterStatus{
					Phase: cloudv1.ClusterPhaseRunning,
				},
			},
			wantUpdated: false,
			wantErr:     false,
		},
		{
			name: "already paused without reason",
			cluster: &cloudv1.Cluster{
				Spec: cloudv1.ClusterSpec{
					Paused: pointer.Bool(true),
				},
				Status: cloudv1.ClusterStatus{
					Phase: cloudv1.ClusterPhaseRunning,
				},
			},
			wantUpdated: false,
			wantErr:     false,
		},
		{
			name: "already paused by other component",
			cluster: &cloudv1.Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Annotations: map[string]string{
						cloudv1.AnnoPauseReconciliationReason: "other",
					},
				},
				Spec: cloudv1.ClusterSpec{
					Paused: pointer.Bool(true),
				},
				Status: cloudv1.ClusterStatus{
					Phase: cloudv1.ClusterPhaseRunning,
				},
			},
			wantUpdated: false,
			wantErr:     true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := runtime.NewScheme()
			_ = cloudv1.AddToScheme(s)
			cli := fake.NewClientBuilder().WithScheme(s).Build()
			updated, err := PauseCluster(context.Background(), cli, tt.cluster)
			if updated != tt.wantUpdated {
				t.Errorf("PauseCluster() updated = %v, want %v", updated, tt.wantUpdated)
			}
			if (err != nil) != tt.wantErr {
				t.Errorf("PauseCluster() error = %v, expectedError %v", err, tt.wantErr)
			}
			if tt.wantUpdated && tt.cluster.GetAnnotations()[cloudv1.AnnoPauseReconciliationReason] != PauseReason {
				t.Errorf("PauseCluster() annotation not set")
			}
		})
	}
}

func Test_isNodeVersionHigher(t *testing.T) {
	tests := []struct {
		name           string
		nodeVer        string
		desiredVer     string
		wantHigher     bool
		wantErr        bool
		wantErrContain string
	}{
		{name: "higher single-digit minor (lexicographic trap)", nodeVer: "1.10", desiredVer: "1.9", wantHigher: true},
		{name: "not higher two-digit vs single-digit minor", nodeVer: "1.9", desiredVer: "1.10", wantHigher: false},
		{name: "higher minor", nodeVer: "1.28", desiredVer: "1.27", wantHigher: true},
		{name: "equal", nodeVer: "1.27", desiredVer: "1.27", wantHigher: false},
		{name: "lower minor", nodeVer: "1.26", desiredVer: "1.27", wantHigher: false},
		{name: "v prefix on node", nodeVer: "v1.28", desiredVer: "1.27", wantHigher: true},

		{name: "both empty", nodeVer: "", desiredVer: "", wantErr: true, wantErrContain: "cannot compare versions"},
		{name: "empty node", nodeVer: "", desiredVer: "1.27", wantErr: true, wantErrContain: "cannot compare versions"},
		{name: "invalid node", nodeVer: "not-a-version", desiredVer: "1.27", wantErr: true, wantErrContain: "compare versions"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := isNodeVersionHigher(tt.nodeVer, tt.desiredVer)
			if tt.wantErr {
				if err == nil {
					t.Fatal("isNodeVersionHigher() expected error, got nil")
				}
				if tt.wantErrContain != "" && !strings.Contains(err.Error(), tt.wantErrContain) {
					t.Fatalf("isNodeVersionHigher() error = %q, want substring %q", err.Error(), tt.wantErrContain)
				}
				if got {
					t.Errorf("isNodeVersionHigher() = true on error, want false")
				}
				return
			}
			if err != nil {
				t.Fatalf("isNodeVersionHigher() unexpected error: %v", err)
			}
			if got != tt.wantHigher {
				t.Errorf("isNodeVersionHigher(%q, %q) = %v, want %v", tt.nodeVer, tt.desiredVer, got, tt.wantHigher)
			}
		})
	}
}
