package tioperator

import (
	"context"
	"testing"

	"github.com/pingcap/tidb-operator/api/v2/core/v1alpha1"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	schemav1 "github.com/tidbcloud/cluster-service-ng/pkg/storage/etcd/schema/v1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
	"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
)

func TestListClusters(t *testing.T) {
	scheme := buildTestScheme(t)

	tests := []struct {
		name          string
		setupObjects  func() []client.Object
		expectedCount int
		expectedErr   bool
	}{
		{
			name: "list multiple clusters",
			setupObjects: func() []client.Object {
				ns1 := &corev1.Namespace{
					ObjectMeta: metav1.ObjectMeta{
						Name: dataplaneNamespace("cluster1"),
						Labels: map[string]string{
							schemav1.LabelUsedForKey: schemav1.ClusterNGServiceType,
						},
					},
				}
				ns2 := &corev1.Namespace{
					ObjectMeta: metav1.ObjectMeta{
						Name: dataplaneNamespace("cluster2"),
						Labels: map[string]string{
							schemav1.LabelUsedForKey: schemav1.ClusterNGServiceType,
						},
					},
				}

				cluster1 := &v1alpha1.Cluster{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "db",
						Namespace: dataplaneNamespace("cluster1"),
					},
					Spec: v1alpha1.ClusterSpec{},
					Status: v1alpha1.ClusterStatus{
						ObservedGeneration: 1,
						Conditions: []metav1.Condition{
							{Type: v1alpha1.ClusterCondAvailable, Status: metav1.ConditionTrue},
						},
					},
				}
				cluster1.Generation = 1

				cluster2 := &v1alpha1.Cluster{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "db",
						Namespace: dataplaneNamespace("cluster2"),
					},
					Spec: v1alpha1.ClusterSpec{},
					Status: v1alpha1.ClusterStatus{
						ObservedGeneration: 1,
						Conditions: []metav1.Condition{
							{Type: v1alpha1.ClusterCondAvailable, Status: metav1.ConditionTrue},
						},
					},
				}
				cluster2.Generation = 1

				pd1 := newPDGroup(t, dataplaneNamespace("cluster1"), testComponentId("pd1"), 3)
				pd2 := newPDGroup(t, dataplaneNamespace("cluster2"), testComponentId("pd2"), 3)

				return []client.Object{ns1, ns2, cluster1, cluster2, pd1, pd2}
			},
			expectedCount: 2,
			expectedErr:   false,
		},
		{
			name: "no clusters",
			setupObjects: func() []client.Object {
				return []client.Object{}
			},
			expectedCount: 0,
			expectedErr:   false,
		},
		{
			name: "namespace without tidb prefix is skipped",
			setupObjects: func() []client.Object {
				ns := &corev1.Namespace{
					ObjectMeta: metav1.ObjectMeta{
						Name: "invalid-namespace",
						Labels: map[string]string{
							schemav1.LabelUsedForKey: schemav1.ClusterNGServiceType,
						},
					},
				}
				return []client.Object{ns}
			},
			expectedCount: 0,
			expectedErr:   false,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			objects := tc.setupObjects()
			cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build()

			ctx := context.Background()
			clusters, err := ListClusters(ctx, cli)

			if tc.expectedErr {
				require.Error(t, err)
			} else {
				require.NoError(t, err)
				assert.Len(t, clusters, tc.expectedCount)
			}
		})
	}
}

func TestSetClusterSuspendComputePreservesSuspendAction(t *testing.T) {
	scheme := buildTestScheme(t)
	cluster := &v1alpha1.Cluster{ObjectMeta: metav1.ObjectMeta{Name: clusterCRName, Namespace: dataplaneNamespace("100")}}
	cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(cluster).Build()
	require.NoError(t, SetClusterSuspendCompute(context.Background(), cli, "100", true))
	updated := &v1alpha1.Cluster{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKeyFromObject(cluster), updated))
	require.NotNil(t, updated.Spec.SuspendAction)
	require.True(t, updated.Spec.SuspendAction.SuspendCompute)
}

func TestIsClusterSuspendComputeObserved(t *testing.T) {
	scheme := buildTestScheme(t)
	tests := []struct {
		name             string
		desired          bool
		suspendActionNil bool
		conditions       []metav1.Condition
		want             bool
	}{
		{name: "suspended", desired: true, want: true, conditions: []metav1.Condition{{Type: v1alpha1.ClusterCondSuspended, Status: metav1.ConditionTrue, ObservedGeneration: 2}}},
		{name: "ready and synced", want: true, conditions: []metav1.Condition{{Type: v1alpha1.ClusterCondSuspended, Status: metav1.ConditionFalse, ObservedGeneration: 2}, {Type: v1alpha1.CondReady, Status: metav1.ConditionTrue, ObservedGeneration: 2}, {Type: v1alpha1.CondSynced, Status: metav1.ConditionTrue, ObservedGeneration: 2}}},
		{name: "ready and synced while unavailable", want: true, conditions: []metav1.Condition{{Type: v1alpha1.ClusterCondAvailable, Status: metav1.ConditionFalse, ObservedGeneration: 2}, {Type: v1alpha1.ClusterCondSuspended, Status: metav1.ConditionFalse, ObservedGeneration: 2}, {Type: v1alpha1.CondReady, Status: metav1.ConditionTrue, ObservedGeneration: 2}, {Type: v1alpha1.CondSynced, Status: metav1.ConditionTrue, ObservedGeneration: 2}}},
		{name: "nil suspend action is effective false", suspendActionNil: true, want: true, conditions: []metav1.Condition{{Type: v1alpha1.ClusterCondSuspended, Status: metav1.ConditionFalse, ObservedGeneration: 2}, {Type: v1alpha1.CondReady, Status: metav1.ConditionTrue, ObservedGeneration: 2}, {Type: v1alpha1.CondSynced, Status: metav1.ConditionTrue, ObservedGeneration: 2}}},
		{name: "available is not enough", want: false, conditions: []metav1.Condition{{Type: v1alpha1.ClusterCondAvailable, Status: metav1.ConditionTrue, ObservedGeneration: 2}, {Type: v1alpha1.ClusterCondSuspended, Status: metav1.ConditionFalse, ObservedGeneration: 2}}},
		{name: "not ready", want: false, conditions: []metav1.Condition{{Type: v1alpha1.ClusterCondSuspended, Status: metav1.ConditionFalse, ObservedGeneration: 2}, {Type: v1alpha1.CondReady, Status: metav1.ConditionFalse, ObservedGeneration: 2}, {Type: v1alpha1.CondSynced, Status: metav1.ConditionTrue, ObservedGeneration: 2}}},
		{name: "not synced", want: false, conditions: []metav1.Condition{{Type: v1alpha1.ClusterCondSuspended, Status: metav1.ConditionFalse, ObservedGeneration: 2}, {Type: v1alpha1.CondReady, Status: metav1.ConditionTrue, ObservedGeneration: 2}, {Type: v1alpha1.CondSynced, Status: metav1.ConditionFalse, ObservedGeneration: 2}}},
		{name: "stale condition", desired: true, conditions: []metav1.Condition{{Type: v1alpha1.ClusterCondSuspended, Status: metav1.ConditionTrue, ObservedGeneration: 1}}},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var suspendAction *v1alpha1.SuspendAction
			if !tt.suspendActionNil {
				suspendAction = &v1alpha1.SuspendAction{SuspendCompute: tt.desired}
			}
			cluster := &v1alpha1.Cluster{
				ObjectMeta: metav1.ObjectMeta{Name: clusterCRName, Namespace: dataplaneNamespace("100"), Generation: 2},
				Spec:       v1alpha1.ClusterSpec{SuspendAction: suspendAction},
				Status:     v1alpha1.ClusterStatus{ObservedGeneration: 2, Conditions: tt.conditions},
			}
			cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(cluster).Build()
			got, err := IsClusterSuspendComputeObserved(context.Background(), cli, "100", tt.desired)
			require.NoError(t, err)
			require.Equal(t, tt.want, got)
		})
	}
}

func TestInspectClusterSuspendComputeReportsPendingSpec(t *testing.T) {
	scheme := buildTestScheme(t)
	cluster := &v1alpha1.Cluster{
		ObjectMeta: metav1.ObjectMeta{Name: clusterCRName, Namespace: dataplaneNamespace("100"), Generation: 2},
		Spec:       v1alpha1.ClusterSpec{SuspendAction: &v1alpha1.SuspendAction{SuspendCompute: true}},
		Status:     v1alpha1.ClusterStatus{ObservedGeneration: 1},
	}
	cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(cluster).Build()

	status, err := InspectClusterSuspendCompute(context.Background(), cli, "100")
	require.NoError(t, err)
	require.True(t, status.SpecDesired)
	require.False(t, status.Observed)
}

func TestUpdateClusterUpgradePolicy(t *testing.T) {
	scheme := buildTestScheme(t)
	revisionHistoryLimit := int32(7)
	cluster := &v1alpha1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      clusterCRName,
			Namespace: dataplaneNamespace("cluster1"),
		},
		Spec: v1alpha1.ClusterSpec{
			UpgradePolicy:        v1alpha1.UpgradePolicyDefault,
			Paused:               true,
			RevisionHistoryLimit: &revisionHistoryLimit,
		},
	}
	patchCalls := 0
	cli := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(cluster).
		WithInterceptorFuncs(interceptor.Funcs{
			Patch: func(
				ctx context.Context,
				underlying client.WithWatch,
				obj client.Object,
				patch client.Patch,
				opts ...client.PatchOption,
			) error {
				patchCalls++
				return underlying.Patch(ctx, obj, patch, opts...)
			},
		}).
		Build()

	err := UpdateClusterUpgradePolicy(
		context.Background(),
		cli,
		"cluster1",
		v1alpha1.UpgradePolicyNoConstraints,
	)
	require.NoError(t, err)

	updated := &v1alpha1.Cluster{}
	require.NoError(t, cli.Get(
		context.Background(),
		client.ObjectKey{Name: clusterCRName, Namespace: dataplaneNamespace("cluster1")},
		updated,
	))
	assert.Equal(t, v1alpha1.UpgradePolicyNoConstraints, updated.Spec.UpgradePolicy)
	assert.True(t, updated.Spec.Paused)
	require.NotNil(t, updated.Spec.RevisionHistoryLimit)
	assert.Equal(t, revisionHistoryLimit, *updated.Spec.RevisionHistoryLimit)
	assert.Equal(t, 1, patchCalls)

	err = UpdateClusterUpgradePolicy(
		context.Background(),
		cli,
		"cluster1",
		v1alpha1.UpgradePolicyNoConstraints,
	)
	require.NoError(t, err)
	assert.Equal(t, 1, patchCalls, "an already-migrated cluster must not be patched again")
}

func TestIsClusterUpgradePolicyObserved(t *testing.T) {
	testCases := []struct {
		name               string
		policy             v1alpha1.UpgradePolicy
		generation         int64
		observedGeneration int64
		expected           bool
	}{
		{
			name:               "policy and generation match",
			policy:             v1alpha1.UpgradePolicyNoConstraints,
			generation:         2,
			observedGeneration: 2,
			expected:           true,
		},
		{
			name:               "policy has not been observed",
			policy:             v1alpha1.UpgradePolicyNoConstraints,
			generation:         2,
			observedGeneration: 1,
			expected:           false,
		},
		{
			name:               "policy does not match",
			policy:             v1alpha1.UpgradePolicyDefault,
			generation:         2,
			observedGeneration: 2,
			expected:           false,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			cluster := &v1alpha1.Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Name:       clusterCRName,
					Namespace:  dataplaneNamespace("cluster1"),
					Generation: tc.generation,
				},
				Spec: v1alpha1.ClusterSpec{
					UpgradePolicy: tc.policy,
				},
				Status: v1alpha1.ClusterStatus{
					ObservedGeneration: tc.observedGeneration,
				},
			}
			cli := fake.NewClientBuilder().
				WithScheme(buildTestScheme(t)).
				WithObjects(cluster).
				Build()

			observed, err := IsClusterUpgradePolicyObserved(
				context.Background(),
				cli,
				"cluster1",
				v1alpha1.UpgradePolicyNoConstraints,
			)
			require.NoError(t, err)
			assert.Equal(t, tc.expected, observed)
		})
	}
}

func TestGetCluster(t *testing.T) {
	scheme := buildTestScheme(t)

	tests := []struct {
		name          string
		clusterId     string
		setupObjects  func(string) []client.Object
		expectedPhase apiv1.SharedPool_Phase
		expectedErr   bool
	}{
		{
			name:      "available cluster",
			clusterId: "test-cluster",
			setupObjects: func(clusterId string) []client.Object {
				cluster := &v1alpha1.Cluster{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "db",
						Namespace: dataplaneNamespace(clusterId),
					},
					Status: v1alpha1.ClusterStatus{
						ObservedGeneration: 1,
						Conditions: []metav1.Condition{
							{Type: v1alpha1.ClusterCondAvailable, Status: metav1.ConditionTrue},
						},
					},
				}
				cluster.Generation = 1

				pd := newPDGroup(t, dataplaneNamespace(clusterId), testComponentId("pd"), 3)
				tikv := newTiKVGroup(t, dataplaneNamespace(clusterId), testComponentId("tikv"), 3)
				tidb := newTiDBGroup(t, dataplaneNamespace(clusterId), testComponentId("tidb"), "tidb", "tidb", 2)

				// Add backup components (available)
				tibr := newTiBR(t, dataplaneNamespace(clusterId), true, true)
				tibrgc := newTiBRGC(t, dataplaneNamespace(clusterId))

				return []client.Object{cluster, pd, tikv, tidb, tibr, tibrgc}
			},
			expectedPhase: apiv1.SharedPool_AVAILABLE,
			expectedErr:   false,
		},
		{
			name:      "paused cluster",
			clusterId: "paused-cluster",
			setupObjects: func(clusterId string) []client.Object {
				cluster := &v1alpha1.Cluster{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "db",
						Namespace: dataplaneNamespace(clusterId),
					},
					Status: v1alpha1.ClusterStatus{
						ObservedGeneration: 1,
						Conditions: []metav1.Condition{
							{Type: v1alpha1.ClusterCondSuspended, Status: metav1.ConditionTrue},
						},
					},
				}
				cluster.Generation = 1

				pd := newPDGroup(t, dataplaneNamespace(clusterId), testComponentId("pd"), 3)
				// Paused clusters return early, so backup components aren't checked
				return []client.Object{cluster, pd}
			},
			expectedPhase: apiv1.SharedPool_UNAVAILABLE,
			expectedErr:   false,
		},
		{
			name:      "pausing cluster",
			clusterId: "pausing-cluster",
			setupObjects: func(clusterId string) []client.Object {
				cluster := &v1alpha1.Cluster{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "db",
						Namespace: dataplaneNamespace(clusterId),
					},
					Spec: v1alpha1.ClusterSpec{
						Paused: true,
					},
					Status: v1alpha1.ClusterStatus{
						ObservedGeneration: 1,
						Conditions: []metav1.Condition{
							{Type: v1alpha1.ClusterCondAvailable, Status: metav1.ConditionTrue},
						},
					},
				}
				cluster.Generation = 1

				pd := newPDGroup(t, dataplaneNamespace(clusterId), testComponentId("pd"), 3)
				// Pausing clusters return early, so backup components aren't checked
				return []client.Object{cluster, pd}
			},
			expectedPhase: apiv1.SharedPool_UNAVAILABLE,
			expectedErr:   false,
		},
		{
			name:      "unavailable cluster - cluster not available",
			clusterId: "unavail-cluster",
			setupObjects: func(clusterId string) []client.Object {
				cluster := &v1alpha1.Cluster{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "db",
						Namespace: dataplaneNamespace(clusterId),
					},
					Status: v1alpha1.ClusterStatus{
						ObservedGeneration: 1,
						Conditions: []metav1.Condition{
							{Type: v1alpha1.ClusterCondAvailable, Status: metav1.ConditionFalse},
						},
					},
				}
				cluster.Generation = 1

				pd := newPDGroup(t, dataplaneNamespace(clusterId), testComponentId("pd"), 3)
				// Unavailable clusters return early, so backup components aren't checked
				return []client.Object{cluster, pd}
			},
			expectedPhase: apiv1.SharedPool_UNAVAILABLE,
			expectedErr:   false,
		},
		{
			name:      "cluster cond unavailable but pd tikv only should be available",
			clusterId: "topology-base-cluster",
			setupObjects: func(clusterId string) []client.Object {
				cluster := &v1alpha1.Cluster{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "db",
						Namespace: dataplaneNamespace(clusterId),
					},
					Status: v1alpha1.ClusterStatus{
						ObservedGeneration: 1,
						Conditions: []metav1.Condition{
							{Type: v1alpha1.ClusterCondAvailable, Status: metav1.ConditionFalse},
						},
					},
				}
				cluster.Generation = 1

				pd := newPDGroup(t, dataplaneNamespace(clusterId), testComponentId("pd"), 3)
				tikv := newTiKVGroup(t, dataplaneNamespace(clusterId), testComponentId("tikv"), 3)

				// Add backup components (available)
				tibr := newTiBR(t, dataplaneNamespace(clusterId), true, true)
				tibrgc := newTiBRGC(t, dataplaneNamespace(clusterId))

				return []client.Object{cluster, pd, tikv, tibr, tibrgc}
			},
			expectedPhase: apiv1.SharedPool_AVAILABLE,
			expectedErr:   false,
		},
		{
			name:      "cluster cond unavailable and pd tikv only without backup components should be available",
			clusterId: "topology-base-no-backup",
			setupObjects: func(clusterId string) []client.Object {
				cluster := &v1alpha1.Cluster{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "db",
						Namespace: dataplaneNamespace(clusterId),
					},
					Status: v1alpha1.ClusterStatus{
						ObservedGeneration: 1,
						Conditions: []metav1.Condition{
							{Type: v1alpha1.ClusterCondAvailable, Status: metav1.ConditionFalse},
						},
					},
				}
				cluster.Generation = 1

				pd := newPDGroup(t, dataplaneNamespace(clusterId), testComponentId("pd"), 3)
				tikv := newTiKVGroup(t, dataplaneNamespace(clusterId), testComponentId("tikv"), 3)

				return []client.Object{cluster, pd, tikv}
			},
			expectedPhase: apiv1.SharedPool_AVAILABLE,
			expectedErr:   false,
		},
		{
			name:      "cluster cond unavailable with no tidb but includes tiproxy should be available",
			clusterId: "topology-no-tidb-with-tiproxy",
			setupObjects: func(clusterId string) []client.Object {
				cluster := &v1alpha1.Cluster{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "db",
						Namespace: dataplaneNamespace(clusterId),
					},
					Status: v1alpha1.ClusterStatus{
						ObservedGeneration: 1,
						Conditions: []metav1.Condition{
							{Type: v1alpha1.ClusterCondAvailable, Status: metav1.ConditionFalse},
						},
					},
				}
				cluster.Generation = 1

				pd := newPDGroup(t, dataplaneNamespace(clusterId), testComponentId("pd"), 3)
				tikv := newTiKVGroup(t, dataplaneNamespace(clusterId), testComponentId("tikv"), 3)
				tiproxy := newTiProxyGroup(t, dataplaneNamespace(clusterId), testComponentId("tiproxy"), 2)

				return []client.Object{cluster, pd, tikv, tiproxy}
			},
			expectedPhase: apiv1.SharedPool_AVAILABLE,
			expectedErr:   false,
		},
		{
			name:      "cluster cond unavailable with no tidb and tiproxy unavailable should still be available",
			clusterId: "topology-no-tidb-with-unready-tiproxy",
			setupObjects: func(clusterId string) []client.Object {
				cluster := &v1alpha1.Cluster{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "db",
						Namespace: dataplaneNamespace(clusterId),
					},
					Status: v1alpha1.ClusterStatus{
						ObservedGeneration: 1,
						Conditions: []metav1.Condition{
							{Type: v1alpha1.ClusterCondAvailable, Status: metav1.ConditionFalse},
						},
					},
				}
				cluster.Generation = 1

				pd := newPDGroup(t, dataplaneNamespace(clusterId), testComponentId("pd"), 3)
				tikv := newTiKVGroup(t, dataplaneNamespace(clusterId), testComponentId("tikv"), 3)
				tiproxy := newTiProxyGroup(t, dataplaneNamespace(clusterId), testComponentId("tiproxy"), 2)
				tiproxy.Status.Conditions = []metav1.Condition{
					{Type: v1alpha1.CondReady, Status: metav1.ConditionFalse},
					{Type: v1alpha1.CondSynced, Status: metav1.ConditionTrue},
				}

				return []client.Object{cluster, pd, tikv, tiproxy}
			},
			expectedPhase: apiv1.SharedPool_AVAILABLE,
			expectedErr:   false,
		},
		{
			name:      "cluster cond unavailable with zero tikv worker should still be available",
			clusterId: "topology-with-zero-tikv-worker",
			setupObjects: func(clusterId string) []client.Object {
				cluster := &v1alpha1.Cluster{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "db",
						Namespace: dataplaneNamespace(clusterId),
					},
					Status: v1alpha1.ClusterStatus{
						ObservedGeneration: 1,
						Conditions: []metav1.Condition{
							{Type: v1alpha1.ClusterCondAvailable, Status: metav1.ConditionFalse},
						},
					},
				}
				cluster.Generation = 1

				pd := newPDGroup(t, dataplaneNamespace(clusterId), testComponentId("pd"), 3)
				tikv := newTiKVGroup(t, dataplaneNamespace(clusterId), testComponentId("tikv"), 3)
				tikvWorker := newTiKVWorkerGroup(t, dataplaneNamespace(clusterId), testComponentId("tikv-worker"), "tikv-worker", 0)
				tikvWorker.Status.Conditions = []metav1.Condition{
					{Type: v1alpha1.CondReady, Status: metav1.ConditionFalse},
					{Type: v1alpha1.CondSynced, Status: metav1.ConditionTrue},
				}

				return []client.Object{cluster, pd, tikv, tikvWorker}
			},
			expectedPhase: apiv1.SharedPool_AVAILABLE,
			expectedErr:   false,
		},
		{
			name:      "cluster cond unavailable with zero tidb worker should still be available",
			clusterId: "topology-with-zero-tidb-worker",
			setupObjects: func(clusterId string) []client.Object {
				cluster := &v1alpha1.Cluster{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "db",
						Namespace: dataplaneNamespace(clusterId),
					},
					Status: v1alpha1.ClusterStatus{
						ObservedGeneration: 1,
						Conditions: []metav1.Condition{
							{Type: v1alpha1.ClusterCondAvailable, Status: metav1.ConditionFalse},
						},
					},
				}
				cluster.Generation = 1

				pd := newPDGroup(t, dataplaneNamespace(clusterId), testComponentId("pd"), 3)
				tikv := newTiKVGroup(t, dataplaneNamespace(clusterId), testComponentId("tikv"), 3)
				tidbWorker := newTiDBGroup(t, dataplaneNamespace(clusterId), testComponentId("tidb-worker"), "tidb-worker", "tidb-worker", 0)
				tidbWorker.Status.Conditions = []metav1.Condition{
					{Type: v1alpha1.CondReady, Status: metav1.ConditionFalse},
					{Type: v1alpha1.CondSynced, Status: metav1.ConditionTrue},
				}

				return []client.Object{cluster, pd, tikv, tidbWorker}
			},
			expectedPhase: apiv1.SharedPool_AVAILABLE,
			expectedErr:   false,
		},
		{
			name:      "cluster with positive tikv worker unavailable should be unavailable",
			clusterId: "topology-with-unready-tikv-worker",
			setupObjects: func(clusterId string) []client.Object {
				cluster := &v1alpha1.Cluster{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "db",
						Namespace: dataplaneNamespace(clusterId),
					},
					Status: v1alpha1.ClusterStatus{
						ObservedGeneration: 1,
						Conditions: []metav1.Condition{
							{Type: v1alpha1.ClusterCondAvailable, Status: metav1.ConditionTrue},
						},
					},
				}
				cluster.Generation = 1

				pd := newPDGroup(t, dataplaneNamespace(clusterId), testComponentId("pd"), 3)
				tikv := newTiKVGroup(t, dataplaneNamespace(clusterId), testComponentId("tikv"), 3)
				tikvWorker := newTiKVWorkerGroup(t, dataplaneNamespace(clusterId), testComponentId("tikv-worker"), "tikv-worker", 2)
				tikvWorker.Status.Conditions = []metav1.Condition{
					{Type: v1alpha1.CondReady, Status: metav1.ConditionFalse},
					{Type: v1alpha1.CondSynced, Status: metav1.ConditionTrue},
				}

				return []client.Object{cluster, pd, tikv, tikvWorker}
			},
			expectedPhase: apiv1.SharedPool_UNAVAILABLE,
			expectedErr:   false,
		},
		{
			name:      "unavailable cluster - component unavailable",
			clusterId: "comp-unavail-cluster",
			setupObjects: func(clusterId string) []client.Object {
				cluster := &v1alpha1.Cluster{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "db",
						Namespace: dataplaneNamespace(clusterId),
					},
					Status: v1alpha1.ClusterStatus{
						ObservedGeneration: 1,
						Conditions: []metav1.Condition{
							{Type: v1alpha1.ClusterCondAvailable, Status: metav1.ConditionTrue},
						},
					},
				}
				cluster.Generation = 1

				pd := newPDGroup(t, dataplaneNamespace(clusterId), testComponentId("pd"), 3)
				// Make PD not ready
				pd.Status.Conditions = []metav1.Condition{
					{Type: v1alpha1.CondReady, Status: metav1.ConditionFalse},
					{Type: v1alpha1.CondSynced, Status: metav1.ConditionTrue},
				}

				// Add backup components (available) - backup check happens after component status check
				tibr := newTiBR(t, dataplaneNamespace(clusterId), true, true)
				tibrgc := newTiBRGC(t, dataplaneNamespace(clusterId))

				return []client.Object{cluster, pd, tibr, tibrgc}
			},
			expectedPhase: apiv1.SharedPool_UNAVAILABLE,
			expectedErr:   false,
		},
		{
			name:      "modifying cluster - component modifying",
			clusterId: "modifying-cluster",
			setupObjects: func(clusterId string) []client.Object {
				cluster := &v1alpha1.Cluster{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "db",
						Namespace: dataplaneNamespace(clusterId),
					},
					Status: v1alpha1.ClusterStatus{
						ObservedGeneration: 1,
						Conditions: []metav1.Condition{
							{Type: v1alpha1.ClusterCondAvailable, Status: metav1.ConditionTrue},
						},
					},
				}
				cluster.Generation = 1

				pd := newPDGroup(t, dataplaneNamespace(clusterId), testComponentId("pd"), 3)
				tikv := newTiKVGroup(t, dataplaneNamespace(clusterId), testComponentId("tikv"), 3)
				// Make TiKV not synced
				tikv.Status.Conditions = []metav1.Condition{
					{Type: v1alpha1.CondReady, Status: metav1.ConditionTrue},
					{Type: v1alpha1.CondSynced, Status: metav1.ConditionFalse},
				}

				// Add backup components (available) - backup check happens after component status check
				tibr := newTiBR(t, dataplaneNamespace(clusterId), true, true)
				tibrgc := newTiBRGC(t, dataplaneNamespace(clusterId))

				return []client.Object{cluster, pd, tikv, tibr, tibrgc}
			},
			expectedPhase: apiv1.SharedPool_MODIFYING,
			expectedErr:   false,
		},
		{
			name:      "cluster not found",
			clusterId: "missing-cluster",
			setupObjects: func(clusterId string) []client.Object {
				// Cluster not found returns early, so backup components aren't checked
				return []client.Object{}
			},
			expectedPhase: apiv1.SharedPool_UNAVAILABLE,
			expectedErr:   false,
		},
		{
			name:      "unavailable cluster - backup components unavailable",
			clusterId: "backup-unavail-cluster",
			setupObjects: func(clusterId string) []client.Object {
				cluster := &v1alpha1.Cluster{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "db",
						Namespace: dataplaneNamespace(clusterId),
					},
					Status: v1alpha1.ClusterStatus{
						ObservedGeneration: 1,
						Conditions: []metav1.Condition{
							{Type: v1alpha1.ClusterCondAvailable, Status: metav1.ConditionTrue},
						},
					},
				}
				cluster.Generation = 1

				pd := newPDGroup(t, dataplaneNamespace(clusterId), testComponentId("pd"), 3)
				tikv := newTiKVGroup(t, dataplaneNamespace(clusterId), testComponentId("tikv"), 3)
				tidb := newTiDBGroup(t, dataplaneNamespace(clusterId), testComponentId("tidb"), "tidb", "tidb", 2)

				// Add backup components that are not ready
				tibr := newTiBR(t, dataplaneNamespace(clusterId), false, false) // Not ready
				tibrgc := newTiBRGC(t, dataplaneNamespace(clusterId))

				return []client.Object{cluster, pd, tikv, tidb, tibr, tibrgc}
			},
			expectedPhase: apiv1.SharedPool_UNAVAILABLE,
			expectedErr:   false,
		},
		{
			name:      "unavailable cluster - backup components not found",
			clusterId: "backup-missing-cluster",
			setupObjects: func(clusterId string) []client.Object {
				cluster := &v1alpha1.Cluster{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "db",
						Namespace: dataplaneNamespace(clusterId),
					},
					Status: v1alpha1.ClusterStatus{
						ObservedGeneration: 1,
						Conditions: []metav1.Condition{
							{Type: v1alpha1.ClusterCondAvailable, Status: metav1.ConditionTrue},
						},
					},
				}
				cluster.Generation = 1

				pd := newPDGroup(t, dataplaneNamespace(clusterId), testComponentId("pd"), 3)
				tikv := newTiKVGroup(t, dataplaneNamespace(clusterId), testComponentId("tikv"), 3)
				tidb := newTiDBGroup(t, dataplaneNamespace(clusterId), testComponentId("tidb"), "tidb", "tidb", 2)

				// Don't add any backup components - they should be missing/not found
				return []client.Object{cluster, pd, tikv, tidb}
			},
			expectedPhase: apiv1.SharedPool_UNAVAILABLE,
			expectedErr:   false,
		},
		{
			name:      "unavailable cluster - tibr not found",
			clusterId: "tibr-missing-cluster",
			setupObjects: func(clusterId string) []client.Object {
				cluster := &v1alpha1.Cluster{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "db",
						Namespace: dataplaneNamespace(clusterId),
					},
					Status: v1alpha1.ClusterStatus{
						ObservedGeneration: 1,
						Conditions: []metav1.Condition{
							{Type: v1alpha1.ClusterCondAvailable, Status: metav1.ConditionTrue},
						},
					},
				}
				cluster.Generation = 1

				pd := newPDGroup(t, dataplaneNamespace(clusterId), testComponentId("pd"), 3)
				tikv := newTiKVGroup(t, dataplaneNamespace(clusterId), testComponentId("tikv"), 3)
				tidb := newTiDBGroup(t, dataplaneNamespace(clusterId), testComponentId("tidb"), "tidb", "tidb", 2)

				// Add only TiBRGC, TiBR is missing
				tibrgc := newTiBRGC(t, dataplaneNamespace(clusterId))

				return []client.Object{cluster, pd, tikv, tidb, tibrgc}
			},
			expectedPhase: apiv1.SharedPool_UNAVAILABLE,
			expectedErr:   false,
		},
		{
			name:      "unavailable cluster - tibrgc not found",
			clusterId: "tibrgc-missing-cluster",
			setupObjects: func(clusterId string) []client.Object {
				cluster := &v1alpha1.Cluster{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "db",
						Namespace: dataplaneNamespace(clusterId),
					},
					Status: v1alpha1.ClusterStatus{
						ObservedGeneration: 1,
						Conditions: []metav1.Condition{
							{Type: v1alpha1.ClusterCondAvailable, Status: metav1.ConditionTrue},
						},
					},
				}
				cluster.Generation = 1

				pd := newPDGroup(t, dataplaneNamespace(clusterId), testComponentId("pd"), 3)
				tikv := newTiKVGroup(t, dataplaneNamespace(clusterId), testComponentId("tikv"), 3)
				tidb := newTiDBGroup(t, dataplaneNamespace(clusterId), testComponentId("tidb"), "tidb", "tidb", 2)

				// Add only TiBR (ready), TiBRGC is missing
				tibr := newTiBR(t, dataplaneNamespace(clusterId), true, true)

				return []client.Object{cluster, pd, tikv, tidb, tibr}
			},
			expectedPhase: apiv1.SharedPool_UNAVAILABLE,
			expectedErr:   false,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			objects := tc.setupObjects(tc.clusterId)
			cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build()

			ctx := context.Background()
			cluster, err := GetCluster(ctx, cli, tc.clusterId)

			if tc.expectedErr {
				require.Error(t, err)
			} else {
				require.NoError(t, err)
				require.NotNil(t, cluster)
				assert.Equal(t, tc.expectedPhase, cluster.Phase)
				assert.Equal(t, tc.clusterId, cluster.Id)
			}
		})
	}
}

func TestGetClusterStatus(t *testing.T) {
	scheme := buildTestScheme(t)

	tests := []struct {
		name          string
		clusterId     string
		setupCluster  func(string) *v1alpha1.Cluster
		expectedPhase apiv1.SharedPool_Phase
	}{
		{
			name:      "available cluster",
			clusterId: "available",
			setupCluster: func(id string) *v1alpha1.Cluster {
				cluster := &v1alpha1.Cluster{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "db",
						Namespace: dataplaneNamespace(id),
					},
					Status: v1alpha1.ClusterStatus{
						ObservedGeneration: 1,
						Conditions: []metav1.Condition{
							{Type: v1alpha1.ClusterCondAvailable, Status: metav1.ConditionTrue},
						},
					},
				}
				cluster.Generation = 1
				return cluster
			},
			expectedPhase: apiv1.SharedPool_AVAILABLE,
		},
		{
			name:      "paused cluster",
			clusterId: "paused",
			setupCluster: func(id string) *v1alpha1.Cluster {
				cluster := &v1alpha1.Cluster{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "db",
						Namespace: dataplaneNamespace(id),
					},
					Status: v1alpha1.ClusterStatus{
						ObservedGeneration: 1,
						Conditions: []metav1.Condition{
							{Type: v1alpha1.ClusterCondSuspended, Status: metav1.ConditionTrue},
						},
					},
				}
				cluster.Generation = 1
				return cluster
			},
			expectedPhase: apiv1.SharedPool_UNAVAILABLE,
		},
		{
			name:      "pausing cluster",
			clusterId: "pausing",
			setupCluster: func(id string) *v1alpha1.Cluster {
				cluster := &v1alpha1.Cluster{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "db",
						Namespace: dataplaneNamespace(id),
					},
					Spec: v1alpha1.ClusterSpec{
						Paused: true,
					},
					Status: v1alpha1.ClusterStatus{
						ObservedGeneration: 1,
						Conditions: []metav1.Condition{
							{Type: v1alpha1.ClusterCondAvailable, Status: metav1.ConditionTrue},
						},
					},
				}
				cluster.Generation = 1
				return cluster
			},
			expectedPhase: apiv1.SharedPool_UNAVAILABLE,
		},
		{
			name:      "unavailable - generation mismatch",
			clusterId: "gen-mismatch",
			setupCluster: func(id string) *v1alpha1.Cluster {
				cluster := &v1alpha1.Cluster{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "db",
						Namespace: dataplaneNamespace(id),
					},
					Status: v1alpha1.ClusterStatus{
						ObservedGeneration: 1,
						Conditions: []metav1.Condition{
							{Type: v1alpha1.ClusterCondAvailable, Status: metav1.ConditionTrue},
						},
					},
				}
				cluster.Generation = 2
				return cluster
			},
			expectedPhase: apiv1.SharedPool_UNAVAILABLE,
		},
		{
			name:      "unavailable - condition false",
			clusterId: "unavail",
			setupCluster: func(id string) *v1alpha1.Cluster {
				cluster := &v1alpha1.Cluster{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "db",
						Namespace: dataplaneNamespace(id),
					},
					Status: v1alpha1.ClusterStatus{
						ObservedGeneration: 1,
						Conditions: []metav1.Condition{
							{Type: v1alpha1.ClusterCondAvailable, Status: metav1.ConditionFalse},
						},
					},
				}
				cluster.Generation = 1
				return cluster
			},
			expectedPhase: apiv1.SharedPool_UNAVAILABLE,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			cluster := tc.setupCluster(tc.clusterId)
			cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(cluster).Build()

			ctx := context.Background()
			phase := getClusterStatus(ctx, cli, tc.clusterId)

			assert.Equal(t, tc.expectedPhase, phase)
		})
	}
}

func TestGetComponents(t *testing.T) {
	clusterId := "test-cluster"
	namespace := dataplaneNamespace(clusterId)
	scheme := buildTestScheme(t)

	tests := []struct {
		name          string
		setupObjects  func() []client.Object
		expectedCount int
		expectedErr   bool
		errorContains string
	}{
		{
			name: "get all component types",
			setupObjects: func() []client.Object {
				tidb := newTiDBGroup(t, namespace, testComponentId("tidb"), "tidb", "tidb", 2)
				tikv := newTiKVGroup(t, namespace, testComponentId("tikv"), 3)
				pd := newPDGroup(t, namespace, testComponentId("pd"), 3)
				tiflash := newTiFlashGroup(t, namespace, testComponentId("tiflash"), "write", "write-tiflash", 2)
				tso := newTsoGroup(t, namespace, testComponentId("tso"), 3)
				scheduling := newSchedulingGroup(t, namespace, testComponentId("scheduling"), 2)
				tiproxy := newTiProxyGroup(t, namespace, testComponentId("tiproxy"), 2)

				return []client.Object{tidb, tikv, pd, tiflash, tso, scheduling, tiproxy}
			},
			expectedCount: 8, // tidb + tikv + pd + tiflash (write + learner) + tso + scheduling + tiproxy
			expectedErr:   false,
		},
		{
			name: "no components",
			setupObjects: func() []client.Object {
				return []client.Object{}
			},
			expectedCount: 0,
			expectedErr:   false,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			objects := tc.setupObjects()
			cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build()

			ctx := context.Background()
			components, err := getComponents(ctx, cli, namespace)

			if tc.expectedErr {
				require.Error(t, err)
				if tc.errorContains != "" {
					assert.Contains(t, err.Error(), tc.errorContains)
				}
			} else {
				require.NoError(t, err)
				assert.Len(t, components, tc.expectedCount)
			}
		})
	}
}
