package br

import (
	"context"
	"errors"
	"testing"
	"time"

	v1alpha1br "github.com/pingcap/tidb-operator/api/v2/br/v1alpha1"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"github.com/tidbcloud/cluster-service-ng/config"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/domain/workflow"
	clusterconfigmocks "github.com/tidbcloud/cluster-service-ng/pkg/clusterconfig/mocks"
	dataplanemocks "github.com/tidbcloud/cluster-service-ng/pkg/dataplane/mocks"
	externalmocks "github.com/tidbcloud/cluster-service-ng/pkg/externalmocks/mocks"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/actions"
	"github.com/tidbcloud/cluster-service-ng/pkg/storage/mysql/schema"
	repomocks "github.com/tidbcloud/cluster-service-ng/pkg/storage/repo/mocks"
	"github.com/tidbcloud/cluster-service-ng/pkg/tioperator"
	"gorm.io/gorm"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

func TestDoRenderTiBRGC_AWS(t *testing.T) {
	action := &applyTiBRGCAction{
		bizConfig:          iniConfig4TiBRGC_AWS(t),
		templateVersionSvc: newTestTemplateVersionSvc(t),
	}

	backupGcSetting := initBackupGcSetting4TiBRGC(t)
	sharedPool := initSharedPool4TiBRGC_AWS(t)

	t2Image := "385595570414.dkr.ecr.us-west-2.amazonaws.com/tidbcloud/tikv:dedicated-next-gen"
	t2Options := map[string]string{
		"file-types-whitelist": "sst",
		"concurrency":          "32",
	}
	t3Image := "385595570414.dkr.ecr.us-west-2.amazonaws.com/tidbcloud/tikv:dedicated-next-gen"
	t3Options := map[string]string{
		"concurrency":     "16",
		"expiration-date": "20250917",
	}

	yaml, err := action.doRenderTiBRGC(context.Background(), backupGcSetting, sharedPool, 2, "8Gi", "60205", testTiBRNodePlacement, t2Image, t2Options, t3Image, t3Options)
	require.NoError(t, err)
	assert.YAMLEq(t, tibrgcCR_AWS, yaml)
}

const tibrgcCR_AWS = `
apiVersion: br.pingcap.com/v1alpha1
kind: TiBRGC
metadata:
  name: tibrgc-default
  namespace: tidb1170307771421593
spec:
  cluster:
    name: db
  gcStrategy:
    type: "tiered-storage"
    tieredStrategies:
    - name: "to-t2-storage"
      timeThresholdDays: 7
      schedule: "0 0 * * *"
      resources:
        cpu: 2
        memory: "8Gi"
      options: ["--concurrency=32", "--file-types-whitelist=sst"]
    - name: "to-t3-storage"
      timeThresholdDays: 5
      schedule: "30 0 * * *"
      resources:
        cpu: 2
        memory: "8Gi"
      volumes:
      - name: "data"
        mounts:
        - type: "data"
          mountPath: "/data1"
        storage: 500Gi
        storageClassName: cloud-ssd
      options: ["--concurrency=16", "--expiration-date=20250917"]
  overlay:
    pods:
    - name: to-t2-storage
      overlay:
        pod:
          metadata:
            labels:
              tags.tidbcloud.com/tenant: "60205"
              tags.tidbcloud.com/component: tibrgc
              tags.tidbcloud.com/resource-pool: "1170307771421593"
          spec:
            nodeSelector:
              node-spec: "4C16G"
              tidbcloud.com/node-pool-k8s-version: "1.33"
            priorityClassName: tidb-ng-component-critical
            serviceAccountName: tidb-cluster
            securityContext:
              runAsUser: 1000
              runAsGroup: 2000
              fsGroup: 2000
              runAsNonRoot: true
            containers:
              - name: to-t2-storage
                image: 385595570414.dkr.ecr.us-west-2.amazonaws.com/tidbcloud/tikv:dedicated-next-gen
                env:
                - name: TZ
                  value: "UTC"
                - name: DFS_PREFIX
                  value: cse
                - name: DFS_S3_BUCKET
                  value: tidbcloud-04859f504e4dcbbedbd7065527d9837c800ed47f1902d190a82de
                - name: DFS_S3_REGION
                  value: us-east-1
                - name: RUST_BACKTRACE
                  value: "full"
                securityContext:
                  readOnlyRootFilesystem: true
                  allowPrivilegeEscalation: false
                  privileged: false
                  capabilities:
                    drop:
                      - NET_RAW
                volumeMounts:
                - mountPath: /work-dir
                  name: tmp-volume
                workingDir: /work-dir
            volumes:
            - emptyDir: {}
              name: tmp-volume
            tolerations:
            - effect: NoSchedule
              key: dedicated
              operator: Equal
              value: worker
    - name: to-t3-storage
      overlay:
        volumeClaims:
        - name: data
          volumeClaim:
            metadata:
              labels:
                tags.tidbcloud.com/tenant: "60205"
                tags.tidbcloud.com/component: tibrgc
                tags.tidbcloud.com/resource-pool: "1170307771421593"
        pod:
          metadata:
            labels:
              tags.tidbcloud.com/tenant: "60205"
              tags.tidbcloud.com/component: tibrgc
              tags.tidbcloud.com/resource-pool: "1170307771421593"
          spec:
            nodeSelector:
              node-spec: "4C16G"
              tidbcloud.com/node-pool-k8s-version: "1.33"
            priorityClassName: tidb-ng-component-critical
            serviceAccountName: tidb-cluster
            securityContext:
              runAsUser: 1000
              runAsGroup: 2000
              fsGroup: 2000
              runAsNonRoot: true
            containers:
              - name: to-t3-storage
                image: 385595570414.dkr.ecr.us-west-2.amazonaws.com/tidbcloud/tikv:dedicated-next-gen
                env:
                - name: TZ
                  value: "UTC"
                - name: DFS_PREFIX
                  value: cse
                - name: DFS_S3_BUCKET
                  value: tidbcloud-04859f504e4dcbbedbd7065527d9837c800ed47f1902d190a82de
                - name: DFS_S3_REGION
                  value: us-east-1
                - name: RUST_BACKTRACE
                  value: "full"
                securityContext:
                  readOnlyRootFilesystem: true
                  allowPrivilegeEscalation: false
                  privileged: false
                  capabilities:
                    drop:
                      - NET_RAW
            tolerations:
            - effect: NoSchedule
              key: dedicated
              operator: Equal
              value: worker
`

func TestDoRenderTiBRGC_ALICLOUD(t *testing.T) {
	action := &applyTiBRGCAction{
		bizConfig:          iniConfig4TiBRGC_ALICLOUD(t),
		templateVersionSvc: newTestTemplateVersionSvc(t),
	}

	backupGcSetting := initBackupGcSetting4TiBRGC(t)
	sharedPool := initSharedPool4TiBRGC_ALICLOUD(t)

	t2Image := "385595570414.dkr.ecr.us-west-2.amazonaws.com/tidbcloud/tikv:dedicated-next-gen"
	t2Options := map[string]string{
		"file-types-whitelist": "sst",
		"concurrency":          "32",
	}
	t3Image := "385595570414.dkr.ecr.us-west-2.amazonaws.com/tidbcloud/tikv:dedicated-next-gen"
	t3Options := map[string]string{
		"concurrency":     "16",
		"expiration-date": "20250917",
	}

	yaml, err := action.doRenderTiBRGC(context.Background(), backupGcSetting, sharedPool, 2, "8Gi", "60205", testTiBRNodePlacement, t2Image, t2Options, t3Image, t3Options)
	require.NoError(t, err)
	assert.YAMLEq(t, tibrgcCR_ALICLOUD, yaml)
}

const tibrgcCR_ALICLOUD = `
apiVersion: br.pingcap.com/v1alpha1
kind: TiBRGC
metadata:
  name: tibrgc-default
  namespace: tidb1170307771421593
spec:
  cluster:
    name: db
  gcStrategy:
    type: "tiered-storage"
    tieredStrategies:
    - name: "to-t2-storage"
      timeThresholdDays: 7
      schedule: "0 0 * * *"
      resources:
        cpu: 2
        memory: "8Gi"
      options: ["--concurrency=32", "--file-types-whitelist=sst"]
    - name: "to-t3-storage"
      timeThresholdDays: 5
      schedule: "30 0 * * *"
      resources:
        cpu: 2
        memory: "8Gi"
      volumes:
      - name: "data"
        mounts:
        - type: "data"
          mountPath: "/data1"
        storage: 500Gi
        storageClassName: cloud-ssd
      options: ["--concurrency=16", "--expiration-date=20250917"]
  overlay:
    pods:
    - name: to-t2-storage
      overlay:
        pod:
          metadata:
            labels:
              tags.tidbcloud.com/tenant: "60205"
              tags.tidbcloud.com/component: tibrgc
              tags.tidbcloud.com/resource-pool: "1170307771421593"
          spec:
            nodeSelector:
              node-spec: "4C16G"
              tidbcloud.com/node-pool-k8s-version: "1.33"
            priorityClassName: tidb-ng-component-critical
            serviceAccountName: tidb-cluster
            securityContext:
              runAsUser: 1000
              runAsGroup: 2000
              fsGroup: 2000
              runAsNonRoot: true
            containers:
              - name: to-t2-storage
                image: 385595570414.dkr.ecr.us-west-2.amazonaws.com/tidbcloud/tikv:dedicated-next-gen
                env:
                - name: TZ
                  value: "UTC"
                - name: DFS_PREFIX
                  value: cse
                - name: DFS_S3_BUCKET
                  value: tidbcloud-04859f504e4dcbbedbd7065527d9837c800ed47f1902d190a82de
                - name: DFS_S3_REGION
                  value: us-east-1
                - name: RUST_BACKTRACE
                  value: "full"
                - name: DFS_S3_ENDPOINT
                  value: oss-us-east-1-internal.aliyuncs.com
                - name: ALIBABA_CLOUD_REGION_ID
                  value: us-east-1
                securityContext:
                  readOnlyRootFilesystem: true
                  allowPrivilegeEscalation: false
                  privileged: false
                  capabilities:
                    drop:
                      - NET_RAW
                volumeMounts:
                - mountPath: /work-dir
                  name: tmp-volume
                workingDir: /work-dir
            volumes:
            - emptyDir: {}
              name: tmp-volume
            tolerations:
            - effect: NoSchedule
              key: dedicated
              operator: Equal
              value: worker
    - name: to-t3-storage
      overlay:
        volumeClaims:
        - name: data
          volumeClaim:
            metadata:
              labels:
                tags.tidbcloud.com/tenant: "60205"
                tags.tidbcloud.com/component: tibrgc
                tags.tidbcloud.com/resource-pool: "1170307771421593"
        pod:
          metadata:
            labels:
              tags.tidbcloud.com/tenant: "60205"
              tags.tidbcloud.com/component: tibrgc
              tags.tidbcloud.com/resource-pool: "1170307771421593"
          spec:
            nodeSelector:
              node-spec: "4C16G"
              tidbcloud.com/node-pool-k8s-version: "1.33"
            priorityClassName: tidb-ng-component-critical
            serviceAccountName: tidb-cluster
            securityContext:
              runAsUser: 1000
              runAsGroup: 2000
              fsGroup: 2000
              runAsNonRoot: true
            containers:
              - name: to-t3-storage
                image: 385595570414.dkr.ecr.us-west-2.amazonaws.com/tidbcloud/tikv:dedicated-next-gen
                env:
                - name: TZ
                  value: "UTC"
                - name: DFS_PREFIX
                  value: cse
                - name: DFS_S3_BUCKET
                  value: tidbcloud-04859f504e4dcbbedbd7065527d9837c800ed47f1902d190a82de
                - name: DFS_S3_REGION
                  value: us-east-1
                - name: RUST_BACKTRACE
                  value: "full"
                - name: DFS_S3_ENDPOINT
                  value: oss-us-east-1-internal.aliyuncs.com
                - name: ALIBABA_CLOUD_REGION_ID
                  value: us-east-1
                securityContext:
                  readOnlyRootFilesystem: true
                  allowPrivilegeEscalation: false
                  privileged: false
                  capabilities:
                    drop:
                      - NET_RAW
            tolerations:
            - effect: NoSchedule
              key: dedicated
              operator: Equal
              value: worker
`

func initBackupGcSetting4TiBRGC(t *testing.T) *entity.BackupGcSetting {
	t.Helper()
	return &entity.BackupGcSetting{
		BackupGcSetting: schema.BackupGcSetting{
			MinT2DataAgeDays: 7,
			MinT3DataAgeDays: 5,
			Model: gorm.Model{
				CreatedAt: time.Date(2025, 9, 17, 0, 0, 0, 0, time.UTC),
			},
		},
	}
}

func initSharedPool4TiBRGC_AWS(t *testing.T) *entity.SharedPool {
	t.Helper()
	return &entity.SharedPool{
		SharedPool: apiv1.SharedPool{
			Id: "1170307771421593",
			ObjectStorage: &apiv1.SharedPool_ObjectStorage{
				Bucket: "tidbcloud-04859f504e4dcbbedbd7065527d9837c800ed47f1902d190a82de",
				Region: "us-east-1",
			},
			Provider: &apiv1.Provider{
				Type:   apiv1.ProviderType_AWS,
				Region: "us-east-1",
			},
		},
	}
}

func initSharedPool4TiBRGC_ALICLOUD(t *testing.T) *entity.SharedPool {
	t.Helper()
	return &entity.SharedPool{
		SharedPool: apiv1.SharedPool{
			Id: "1170307771421593",
			ObjectStorage: &apiv1.SharedPool_ObjectStorage{
				Bucket: "tidbcloud-04859f504e4dcbbedbd7065527d9837c800ed47f1902d190a82de",
				Region: "us-east-1",
			},
			Provider: &apiv1.Provider{
				Type:   apiv1.ProviderType_ALICLOUD,
				Region: "us-east-1",
			},
		},
	}
}

func TestGetMinT2DataAgeDays(t *testing.T) {
	tests := []struct {
		name         string
		cfg          *config.TiBRConfig
		fallbackDays uint32
		expected     uint32
	}{
		{
			name:         "nil config uses fallback",
			cfg:          nil,
			fallbackDays: 7,
			expected:     7,
		},
		{
			name: "zero config uses fallback",
			cfg: &config.TiBRConfig{
				MinT2DataAgeDays: 0,
			},
			fallbackDays: 7,
			expected:     7,
		},
		{
			name: "positive config overrides fallback",
			cfg: &config.TiBRConfig{
				MinT2DataAgeDays: 1,
			},
			fallbackDays: 7,
			expected:     1,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.Equal(t, tt.expected, getMinT2DataAgeDays(tt.cfg, tt.fallbackDays))
		})
	}
}

func iniConfig4TiBRGC_AWS(t *testing.T) *config.BizConfig {
	t.Helper()
	return &config.BizConfig{
		DefaultTiBRConfig: config.TiBRConfig{
			ArchiveBackupStorageSize: "500Gi",
			StorageClass:             DefaultStorageClass,
		},
	}
}

func iniConfig4TiBRGC_ALICLOUD(t *testing.T) *config.BizConfig {
	t.Helper()
	return &config.BizConfig{
		DefaultTiBRConfig: config.TiBRConfig{
			ArchiveBackupStorageSize: "500Gi",
			StorageClass:             DefaultStorageClass,
		},
	}
}

func TestApplyTiBRGCSkipsDeletedPoolWhileGCStatusIsRunning(t *testing.T) {
	sharedPoolID := "deleted-pool"
	deletedAt := time.Now()
	gcSetting := &entity.BackupGcSetting{BackupGcSetting: schema.BackupGcSetting{
		SharedPoolID: sharedPoolID,
		Status:       schema.BackupGcStatusSPRunning,
	}}
	sharedPool := &entity.SharedPool{
		SharedPool: apiv1.SharedPool{Id: sharedPoolID},
		DeletedAt:  &deletedAt,
	}
	gcRepo := repomocks.NewBackupGcSettingRepo(t)
	gcRepo.EXPECT().Load(mock.Anything, sharedPoolID).Return(gcSetting, nil).Twice()
	sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
	sharedPoolRepo.EXPECT().Load(mock.Anything, sharedPoolID, true).Return(sharedPool, nil).Twice()
	action := &applyTiBRGCAction{
		gcRepo:           gcRepo,
		sharedPoolRepo:   sharedPoolRepo,
		dataplaneManager: dataplanemocks.NewManager(t),
	}
	args := actions.ActionArgs{workflow.SharedPoolIdKey: actions.ActionArgString(sharedPoolID)}

	require.NoError(t, action.Start(args))
	finished, err := action.CheckFinish(args)
	require.NoError(t, err)
	require.True(t, finished)
}

func TestApplyTiBRGCAction_CheckFinish(t *testing.T) {
	tests := []struct {
		name             string
		args             actions.ActionArgs
		gcRepo           *repomocks.BackupGcSettingRepo
		sharedPoolRepo   *repomocks.SharedPoolRepo
		dataplaneManager *dataplanemocks.Manager
		mockClient       *externalmocks.Client
		expectedResult   bool
		expectError      bool
		errorContains    string
	}{
		{
			name: "missing shared pool ID",
			args: actions.ActionArgs{},
			gcRepo: func() *repomocks.BackupGcSettingRepo {
				mock := repomocks.NewBackupGcSettingRepo(t)
				return mock
			}(),
			sharedPoolRepo: func() *repomocks.SharedPoolRepo {
				mock := repomocks.NewSharedPoolRepo(t)
				return mock
			}(),
			dataplaneManager: func() *dataplanemocks.Manager {
				mock := dataplanemocks.NewManager(t)
				return mock
			}(),
			mockClient:     nil,
			expectedResult: false,
			expectError:    true,
			errorContains:  "failed to get sharedPool id",
		},
		{
			name: "backup gc setting load fails",
			args: actions.ActionArgs{
				workflow.SharedPoolIdKey: actions.ActionArgString("test-sharedpool"),
			},
			gcRepo: func() *repomocks.BackupGcSettingRepo {
				mock := repomocks.NewBackupGcSettingRepo(t)
				mock.EXPECT().Load(context.TODO(), "test-sharedpool").Return(nil, errors.New("database error"))
				return mock
			}(),
			sharedPoolRepo: func() *repomocks.SharedPoolRepo {
				mock := repomocks.NewSharedPoolRepo(t)
				return mock
			}(),
			dataplaneManager: func() *dataplanemocks.Manager {
				mock := dataplanemocks.NewManager(t)
				return mock
			}(),
			mockClient:     nil,
			expectedResult: false,
			expectError:    true,
			errorContains:  "failed to load backup gc setting",
		},
		{
			name: "tibrgc not applicable - status not in applicable statuses",
			args: actions.ActionArgs{
				workflow.SharedPoolIdKey: actions.ActionArgString("test-sharedpool"),
			},
			gcRepo: func() *repomocks.BackupGcSettingRepo {
				mock := repomocks.NewBackupGcSettingRepo(t)
				backupGcSetting := &entity.BackupGcSetting{
					BackupGcSetting: schema.BackupGcSetting{
						Status: schema.BackupGcStatusSPDeleted, // Not in applicable statuses
					},
				}
				mock.EXPECT().Load(context.TODO(), "test-sharedpool").Return(backupGcSetting, nil)
				return mock
			}(),
			sharedPoolRepo: func() *repomocks.SharedPoolRepo {
				mock := repomocks.NewSharedPoolRepo(t)
				return mock
			}(),
			dataplaneManager: func() *dataplanemocks.Manager {
				mock := dataplanemocks.NewManager(t)
				return mock
			}(),
			mockClient:     nil,
			expectedResult: true, // Should return true when not applicable
			expectError:    false,
		},
		{
			name: "shared pool load fails",
			args: actions.ActionArgs{
				workflow.SharedPoolIdKey: actions.ActionArgString("test-sharedpool"),
			},
			gcRepo: func() *repomocks.BackupGcSettingRepo {
				mock := repomocks.NewBackupGcSettingRepo(t)
				backupGcSetting := &entity.BackupGcSetting{
					BackupGcSetting: schema.BackupGcSetting{
						Status: schema.BackupGcStatusSPRunning, // Applicable status
					},
				}
				mock.EXPECT().Load(context.TODO(), "test-sharedpool").Return(backupGcSetting, nil)
				return mock
			}(),
			sharedPoolRepo: func() *repomocks.SharedPoolRepo {
				mock := repomocks.NewSharedPoolRepo(t)
				mock.EXPECT().Load(context.TODO(), "test-sharedpool", true).Return(nil, errors.New("shared pool not found"))
				return mock
			}(),
			dataplaneManager: func() *dataplanemocks.Manager {
				mock := dataplanemocks.NewManager(t)
				return mock
			}(),
			mockClient:     nil,
			expectedResult: false,
			expectError:    true,
			errorContains:  "failed to load sharedPool",
		},
		{
			name: "dataplane manager get client fails",
			args: actions.ActionArgs{
				workflow.SharedPoolIdKey: actions.ActionArgString("test-sharedpool"),
			},
			gcRepo: func() *repomocks.BackupGcSettingRepo {
				mock := repomocks.NewBackupGcSettingRepo(t)
				backupGcSetting := &entity.BackupGcSetting{
					BackupGcSetting: schema.BackupGcSetting{
						Status: schema.BackupGcStatusSPRunning,
					},
				}
				mock.EXPECT().Load(context.TODO(), "test-sharedpool").Return(backupGcSetting, nil)
				return mock
			}(),
			sharedPoolRepo: func() *repomocks.SharedPoolRepo {
				mock := repomocks.NewSharedPoolRepo(t)
				sharedPool := &entity.SharedPool{
					SharedPool: apiv1.SharedPool{
						Id:            "test-sharedpool",
						DataplaneName: "test-dataplane",
					},
				}
				mock.EXPECT().Load(context.TODO(), "test-sharedpool", true).Return(sharedPool, nil)
				return mock
			}(),
			dataplaneManager: func() *dataplanemocks.Manager {
				mock := dataplanemocks.NewManager(t)
				mock.EXPECT().GetClient("test-dataplane").Return(nil, errors.New("dataplane client error"))
				return mock
			}(),
			mockClient:     nil,
			expectedResult: false,
			expectError:    true,
			errorContains:  "failed to get remote client for shared pool",
		},
		{
			name: "tibrgc get fails",
			args: actions.ActionArgs{
				workflow.SharedPoolIdKey: actions.ActionArgString("test-sharedpool"),
			},
			gcRepo: func() *repomocks.BackupGcSettingRepo {
				mock := repomocks.NewBackupGcSettingRepo(t)
				backupGcSetting := &entity.BackupGcSetting{
					BackupGcSetting: schema.BackupGcSetting{
						Status: schema.BackupGcStatusSPRunning,
					},
				}
				mock.EXPECT().Load(context.TODO(), "test-sharedpool").Return(backupGcSetting, nil)
				return mock
			}(),
			sharedPoolRepo: func() *repomocks.SharedPoolRepo {
				mock := repomocks.NewSharedPoolRepo(t)
				sharedPool := &entity.SharedPool{
					SharedPool: apiv1.SharedPool{
						Id:            "test-sharedpool",
						DataplaneName: "test-dataplane",
					},
				}
				mock.EXPECT().Load(context.TODO(), "test-sharedpool", true).Return(sharedPool, nil)
				return mock
			}(),
			dataplaneManager: func() *dataplanemocks.Manager {
				mock := dataplanemocks.NewManager(t)
				mockClient := externalmocks.NewClient(t)
				mockClient.EXPECT().Get(context.TODO(), types.NamespacedName{
					Name:      tioperator.DefaultTiBRGCRName,
					Namespace: "tidbtest-sharedpool",
				}, &v1alpha1br.TiBRGC{}).Return(errors.New("tibrgc not found"))
				mock.EXPECT().GetClient("test-dataplane").Return(mockClient, nil)
				return mock
			}(),
			mockClient:     externalmocks.NewClient(t),
			expectedResult: false,
			expectError:    true,
			errorContains:  "failed to get tibrgc",
		},
		{
			name: "tibrgc not running",
			args: actions.ActionArgs{
				workflow.SharedPoolIdKey: actions.ActionArgString("test-sharedpool"),
			},
			gcRepo: func() *repomocks.BackupGcSettingRepo {
				mock := repomocks.NewBackupGcSettingRepo(t)
				backupGcSetting := &entity.BackupGcSetting{
					BackupGcSetting: schema.BackupGcSetting{
						Status: schema.BackupGcStatusSPRunning,
					},
				}
				mock.EXPECT().Load(context.TODO(), "test-sharedpool").Return(backupGcSetting, nil)
				return mock
			}(),
			sharedPoolRepo: func() *repomocks.SharedPoolRepo {
				mock := repomocks.NewSharedPoolRepo(t)
				sharedPool := &entity.SharedPool{
					SharedPool: apiv1.SharedPool{
						Id:            "test-sharedpool",
						DataplaneName: "test-dataplane",
					},
				}
				mock.EXPECT().Load(context.TODO(), "test-sharedpool", true).Return(sharedPool, nil)
				return mock
			}(),
			dataplaneManager: func() *dataplanemocks.Manager {
				mock := dataplanemocks.NewManager(t)
				mockClient := externalmocks.NewClient(t)
				mockClient.EXPECT().Get(context.TODO(), types.NamespacedName{
					Name:      tioperator.DefaultTiBRGCRName,
					Namespace: "tidbtest-sharedpool",
				}, &v1alpha1br.TiBRGC{}).Run(func(ctx context.Context, key types.NamespacedName, obj client.Object, opts ...client.GetOption) {
					tibrgcCR := obj.(*v1alpha1br.TiBRGC)
					tibrgcCR.Status = v1alpha1br.TiBRGCStatus{
						Phase: "Pending", // Not running phase
					}
				}).Return(nil)
				mock.EXPECT().GetClient("test-dataplane").Return(mockClient, nil)
				return mock
			}(),
			mockClient:     externalmocks.NewClient(t),
			expectedResult: false, // Not finished because not running
			expectError:    false,
		},
		{
			name: "tibrgc running - success",
			args: actions.ActionArgs{
				workflow.SharedPoolIdKey: actions.ActionArgString("test-sharedpool"),
			},
			gcRepo: func() *repomocks.BackupGcSettingRepo {
				mock := repomocks.NewBackupGcSettingRepo(t)
				backupGcSetting := &entity.BackupGcSetting{
					BackupGcSetting: schema.BackupGcSetting{
						Status: schema.BackupGcStatusSPRunning,
					},
				}
				mock.EXPECT().Load(context.TODO(), "test-sharedpool").Return(backupGcSetting, nil)
				return mock
			}(),
			sharedPoolRepo: func() *repomocks.SharedPoolRepo {
				mock := repomocks.NewSharedPoolRepo(t)
				sharedPool := &entity.SharedPool{
					SharedPool: apiv1.SharedPool{
						Id:            "test-sharedpool",
						DataplaneName: "test-dataplane",
					},
				}
				mock.EXPECT().Load(context.TODO(), "test-sharedpool", true).Return(sharedPool, nil)
				return mock
			}(),
			dataplaneManager: func() *dataplanemocks.Manager {
				mock := dataplanemocks.NewManager(t)
				mockClient := externalmocks.NewClient(t)
				mockClient.EXPECT().Get(context.TODO(), types.NamespacedName{
					Name:      tioperator.DefaultTiBRGCRName,
					Namespace: "tidbtest-sharedpool",
				}, &v1alpha1br.TiBRGC{}).Run(func(ctx context.Context, key types.NamespacedName, obj client.Object, opts ...client.GetOption) {
					tibrgcCR := obj.(*v1alpha1br.TiBRGC)
					tibrgcCR.Status = v1alpha1br.TiBRGCStatus{
						Phase: v1alpha1br.TiBRGCPhaseRunning, // Running
					}
				}).Return(nil)
				mock.EXPECT().GetClient("test-dataplane").Return(mockClient, nil)
				return mock
			}(),
			mockClient:     externalmocks.NewClient(t),
			expectedResult: true, // Finished successfully
			expectError:    false,
		},
		{
			name: "tibrgc applicable - creating status",
			args: actions.ActionArgs{
				workflow.SharedPoolIdKey: actions.ActionArgString("test-sharedpool"),
			},
			gcRepo: func() *repomocks.BackupGcSettingRepo {
				mock := repomocks.NewBackupGcSettingRepo(t)
				backupGcSetting := &entity.BackupGcSetting{
					BackupGcSetting: schema.BackupGcSetting{
						Status: schema.BackupGcStatusSPCreating, // Applicable status
					},
				}
				mock.EXPECT().Load(context.TODO(), "test-sharedpool").Return(backupGcSetting, nil)
				return mock
			}(),
			sharedPoolRepo: func() *repomocks.SharedPoolRepo {
				mock := repomocks.NewSharedPoolRepo(t)
				sharedPool := &entity.SharedPool{
					SharedPool: apiv1.SharedPool{
						Id:            "test-sharedpool",
						DataplaneName: "test-dataplane",
					},
				}
				mock.EXPECT().Load(context.TODO(), "test-sharedpool", true).Return(sharedPool, nil)
				return mock
			}(),
			dataplaneManager: func() *dataplanemocks.Manager {
				mock := dataplanemocks.NewManager(t)
				mockClient := externalmocks.NewClient(t)
				mockClient.EXPECT().Get(context.TODO(), types.NamespacedName{
					Name:      tioperator.DefaultTiBRGCRName,
					Namespace: "tidbtest-sharedpool",
				}, &v1alpha1br.TiBRGC{}).Run(func(ctx context.Context, key types.NamespacedName, obj client.Object, opts ...client.GetOption) {
					tibrgcCR := obj.(*v1alpha1br.TiBRGC)
					tibrgcCR.Status = v1alpha1br.TiBRGCStatus{
						Phase: v1alpha1br.TiBRGCPhaseRunning,
					}
				}).Return(nil)
				mock.EXPECT().GetClient("test-dataplane").Return(mockClient, nil)
				return mock
			}(),
			mockClient:     externalmocks.NewClient(t),
			expectedResult: true, // Finished successfully
			expectError:    false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			action := &applyTiBRGCAction{
				gcRepo:           tt.gcRepo,
				sharedPoolRepo:   tt.sharedPoolRepo,
				dataplaneManager: tt.dataplaneManager,
			}

			result, err := action.CheckFinish(tt.args)

			if tt.expectError {
				require.Error(t, err)
				assert.Contains(t, err.Error(), tt.errorContains)
			} else {
				require.NoError(t, err)
			}

			assert.Equal(t, tt.expectedResult, result)
		})
	}
}

func TestGetT2ImageAndOptionsFromConfigCenter(t *testing.T) {
	ctx := context.Background()
	tenantId := "60205"

	t.Run("success with all options", func(t *testing.T) {
		mockClusterConfigClient := clusterconfigmocks.NewClient(t)
		action := &applyTiBRGCAction{
			imageConfig: &config.ImageConfig{
				Registry:   "test-registry",
				RepoPrefix: "test-repo",
			},
			clusterConfigClient: mockClusterConfigClient,
		}

		targetSharedPool := &entity.SharedPool{
			SharedPool: apiv1.SharedPool{
				Id: "test-pool",
			},
		}

		expectedVersion := "v1.0.0"
		expectedConfig := &entity.ComponentConfiguration{}
		expectedConfig.ImageRepo = "tidbcloud/tikv"
		expectedConfig.ImageTag = "test-tag"
		expectedConfig.ConfigurationFileContent = `
concurrency = 10
file-types-whitelist = "sst,txn"
custom-option = true
start-time-safe-interval = "1h"
`

		mockClusterConfigClient.EXPECT().GetBRComponentVersionAndConfig(ctx, targetSharedPool, tenantId, apiv1.ComponentType_TIBRGC_T2_DFSGC).
			Return(expectedVersion, expectedConfig, nil)

		image, options, err := action.getT2ImageAndOptionsFromConfigCenter(ctx, targetSharedPool, tenantId, 4)

		require.NoError(t, err)
		assert.Equal(t, "test-registry/test-repo/tikv:test-tag", image)
		assert.Equal(t, map[string]string{
			"concurrency":          "10",
			"file-types-whitelist": "sst,txn",
			"custom-option":        "true",
			// start-time-safe-interval should be removed
		}, options)
	})

	t.Run("get component config fails", func(t *testing.T) {
		mockClusterConfigClient := clusterconfigmocks.NewClient(t)
		action := &applyTiBRGCAction{
			clusterConfigClient: mockClusterConfigClient,
		}

		targetSharedPool := &entity.SharedPool{
			SharedPool: apiv1.SharedPool{
				Id: "test-pool",
			},
		}

		expectedErr := errors.New("config center error")
		mockClusterConfigClient.EXPECT().GetBRComponentVersionAndConfig(ctx, targetSharedPool, tenantId, apiv1.ComponentType_TIBRGC_T2_DFSGC).
			Return("", (*entity.ComponentConfiguration)(nil), expectedErr)

		image, options, err := action.getT2ImageAndOptionsFromConfigCenter(ctx, targetSharedPool, tenantId, 4)

		assert.Empty(t, image)
		assert.Nil(t, options)
		require.Error(t, err)
		assert.Contains(t, err.Error(), "failed to get TIBRGC_T2_DFSGC version and config")
	})

	t.Run("toml unmarshal fails", func(t *testing.T) {
		mockClusterConfigClient := clusterconfigmocks.NewClient(t)
		action := &applyTiBRGCAction{
			imageConfig: &config.ImageConfig{
				Registry:   "test-registry",
				RepoPrefix: "test-repo",
			},
			clusterConfigClient: mockClusterConfigClient,
		}

		targetSharedPool := &entity.SharedPool{
			SharedPool: apiv1.SharedPool{
				Id: "test-pool",
			},
		}

		expectedVersion := "v1.0.0"
		expectedConfig := &entity.ComponentConfiguration{}
		expectedConfig.ImageRepo = "tidbcloud/tikv"
		expectedConfig.ImageTag = "test-tag"
		expectedConfig.ConfigurationFileContent = `invalid toml content [[[`

		mockClusterConfigClient.EXPECT().GetBRComponentVersionAndConfig(ctx, targetSharedPool, tenantId, apiv1.ComponentType_TIBRGC_T2_DFSGC).
			Return(expectedVersion, expectedConfig, nil)

		image, options, err := action.getT2ImageAndOptionsFromConfigCenter(ctx, targetSharedPool, tenantId, 4)

		assert.Empty(t, image)
		assert.Nil(t, options)
		require.Error(t, err)
		assert.Contains(t, err.Error(), "failed to unmarshal options")
	})

	t.Run("defaults applied when options missing", func(t *testing.T) {
		mockClusterConfigClient := clusterconfigmocks.NewClient(t)
		action := &applyTiBRGCAction{
			imageConfig: &config.ImageConfig{
				Registry:   "test-registry",
				RepoPrefix: "test-repo",
			},
			clusterConfigClient: mockClusterConfigClient,
		}

		targetSharedPool := &entity.SharedPool{
			SharedPool: apiv1.SharedPool{
				Id: "test-pool",
			},
		}

		expectedVersion := "v1.0.0"
		expectedConfig := &entity.ComponentConfiguration{}
		expectedConfig.ImageRepo = "tidbcloud/tikv"
		expectedConfig.ImageTag = "test-tag"
		expectedConfig.ConfigurationFileContent = `
custom-option = "custom-value"
`

		mockClusterConfigClient.EXPECT().GetBRComponentVersionAndConfig(ctx, targetSharedPool, tenantId, apiv1.ComponentType_TIBRGC_T2_DFSGC).
			Return(expectedVersion, expectedConfig, nil)

		image, options, err := action.getT2ImageAndOptionsFromConfigCenter(ctx, targetSharedPool, tenantId, 4)

		require.NoError(t, err)
		assert.Equal(t, "test-registry/test-repo/tikv:test-tag", image)
		assert.Equal(t, map[string]string{
			"custom-option":        "custom-value",
			"concurrency":          "64", // 4 * 16 = 64
			"file-types-whitelist": "sst",
		}, options)
	})
}

func TestGetT3ImageAndOptionsFromConfigCenter(t *testing.T) {
	ctx := context.Background()
	tenantId := "60205"

	t.Run("success with all options", func(t *testing.T) {
		mockClusterConfigClient := clusterconfigmocks.NewClient(t)
		action := &applyTiBRGCAction{
			imageConfig: &config.ImageConfig{
				Registry:   "test-registry",
				RepoPrefix: "test-repo",
			},
			clusterConfigClient: mockClusterConfigClient,
		}

		targetSharedPool := &entity.SharedPool{
			SharedPool: apiv1.SharedPool{
				Id: "test-pool",
			},
		}

		backupGcSetting := &entity.BackupGcSetting{
			BackupGcSetting: schema.BackupGcSetting{
				MinT2DataAgeDays: 7,
				MinT3DataAgeDays: 5,
				Model: gorm.Model{
					CreatedAt: time.Date(2024, 12, 25, 0, 0, 0, 0, time.UTC),
				},
			},
		}

		expectedVersion := "v1.0.0"
		expectedConfig := &entity.ComponentConfiguration{}
		expectedConfig.ImageRepo = "tidbcloud/tikv"
		expectedConfig.ImageTag = "test-tag"
		expectedConfig.ConfigurationFileContent = `
concurrency = 16
custom-option = "custom-value"
bool-value = false
start-archive-duration = "2h"
data-dir = "/data"
`

		mockClusterConfigClient.EXPECT().GetBRComponentVersionAndConfig(ctx, targetSharedPool, tenantId, apiv1.ComponentType_TIBRGC_T3_ARCHIVE).
			Return(expectedVersion, expectedConfig, nil)

		image, options, err := action.getT3ImageAndOptionsFromConfigCenter(ctx, targetSharedPool, tenantId, backupGcSetting, 4)

		require.NoError(t, err)
		assert.Equal(t, "test-registry/test-repo/tikv:test-tag", image)
		assert.Equal(t, map[string]string{
			"concurrency":     "16",
			"custom-option":   "custom-value",
			"bool-value":      "false",
			"expiration-date": "20241225", // Formatted CreatedAt
			// start-archive-duration and data-dir should be removed
		}, options)
	})

	t.Run("get component config fails", func(t *testing.T) {
		mockClusterConfigClient := clusterconfigmocks.NewClient(t)
		action := &applyTiBRGCAction{
			clusterConfigClient: mockClusterConfigClient,
		}

		targetSharedPool := &entity.SharedPool{
			SharedPool: apiv1.SharedPool{
				Id: "test-pool",
			},
		}

		backupGcSetting := &entity.BackupGcSetting{
			BackupGcSetting: schema.BackupGcSetting{
				MinT2DataAgeDays: 7,
				MinT3DataAgeDays: 5,
				Model: gorm.Model{
					CreatedAt: time.Date(2024, 12, 25, 0, 0, 0, 0, time.UTC),
				},
			},
		}

		expectedErr := errors.New("config center error")
		mockClusterConfigClient.EXPECT().GetBRComponentVersionAndConfig(ctx, targetSharedPool, tenantId, apiv1.ComponentType_TIBRGC_T3_ARCHIVE).
			Return("", (*entity.ComponentConfiguration)(nil), expectedErr)

		image, options, err := action.getT3ImageAndOptionsFromConfigCenter(ctx, targetSharedPool, tenantId, backupGcSetting, 4)

		assert.Empty(t, image)
		assert.Nil(t, options)
		require.Error(t, err)
		assert.Contains(t, err.Error(), "failed to get TIBRGC_T3_ARCHIVE version and config")
	})

	t.Run("toml unmarshal fails", func(t *testing.T) {
		mockClusterConfigClient := clusterconfigmocks.NewClient(t)
		action := &applyTiBRGCAction{
			imageConfig: &config.ImageConfig{
				Registry:   "test-registry",
				RepoPrefix: "test-repo",
			},
			clusterConfigClient: mockClusterConfigClient,
		}

		targetSharedPool := &entity.SharedPool{
			SharedPool: apiv1.SharedPool{
				Id: "test-pool",
			},
		}

		backupGcSetting := &entity.BackupGcSetting{
			BackupGcSetting: schema.BackupGcSetting{
				MinT2DataAgeDays: 7,
				MinT3DataAgeDays: 5,
				Model: gorm.Model{
					CreatedAt: time.Date(2024, 12, 25, 0, 0, 0, 0, time.UTC),
				},
			},
		}

		expectedVersion := "v1.0.0"
		expectedConfig := &entity.ComponentConfiguration{}
		expectedConfig.ImageRepo = "tidbcloud/tikv"
		expectedConfig.ImageTag = "test-tag"
		expectedConfig.ConfigurationFileContent = `invalid toml content [[[`

		mockClusterConfigClient.EXPECT().GetBRComponentVersionAndConfig(ctx, targetSharedPool, tenantId, apiv1.ComponentType_TIBRGC_T3_ARCHIVE).
			Return(expectedVersion, expectedConfig, nil)

		image, options, err := action.getT3ImageAndOptionsFromConfigCenter(ctx, targetSharedPool, tenantId, backupGcSetting, 4)

		assert.Empty(t, image)
		assert.Nil(t, options)
		require.Error(t, err)
		assert.Contains(t, err.Error(), "failed to unmarshal options")
	})

	t.Run("defaults applied when options missing", func(t *testing.T) {
		mockClusterConfigClient := clusterconfigmocks.NewClient(t)
		action := &applyTiBRGCAction{
			imageConfig: &config.ImageConfig{
				Registry:   "test-registry",
				RepoPrefix: "test-repo",
			},
			clusterConfigClient: mockClusterConfigClient,
		}

		targetSharedPool := &entity.SharedPool{
			SharedPool: apiv1.SharedPool{
				Id: "test-pool",
			},
		}

		backupGcSetting := &entity.BackupGcSetting{
			BackupGcSetting: schema.BackupGcSetting{
				MinT2DataAgeDays: 7,
				MinT3DataAgeDays: 5,
				Model: gorm.Model{
					CreatedAt: time.Date(2024, 12, 25, 0, 0, 0, 0, time.UTC),
				},
			},
		}

		expectedVersion := "v1.0.0"
		expectedConfig := &entity.ComponentConfiguration{}
		expectedConfig.ImageRepo = "tidbcloud/tikv"
		expectedConfig.ImageTag = "test-tag"
		expectedConfig.ConfigurationFileContent = `
custom-option = "custom-value"
`

		mockClusterConfigClient.EXPECT().GetBRComponentVersionAndConfig(ctx, targetSharedPool, tenantId, apiv1.ComponentType_TIBRGC_T3_ARCHIVE).
			Return(expectedVersion, expectedConfig, nil)

		image, options, err := action.getT3ImageAndOptionsFromConfigCenter(ctx, targetSharedPool, tenantId, backupGcSetting, 4)

		require.NoError(t, err)
		assert.Equal(t, "test-registry/test-repo/tikv:test-tag", image)
		assert.Equal(t, map[string]string{
			"custom-option":   "custom-value",
			"expiration-date": "20241225", // Formatted CreatedAt
			"concurrency":     "32",       // 4 * 8 = 32 (T3 uses cpu*8 instead of cpu*16)
		}, options)
	})

	t.Run("forbidden options removed and expiration-date from config center is preserved", func(t *testing.T) {
		mockClusterConfigClient := clusterconfigmocks.NewClient(t)
		action := &applyTiBRGCAction{
			imageConfig: &config.ImageConfig{
				Registry:   "test-registry",
				RepoPrefix: "test-repo",
			},
			clusterConfigClient: mockClusterConfigClient,
		}

		targetSharedPool := &entity.SharedPool{
			SharedPool: apiv1.SharedPool{
				Id: "test-pool",
			},
		}

		backupGcSetting := &entity.BackupGcSetting{
			BackupGcSetting: schema.BackupGcSetting{
				MinT2DataAgeDays: 7,
				MinT3DataAgeDays: 5,
				Model: gorm.Model{
					CreatedAt: time.Date(2024, 12, 25, 0, 0, 0, 0, time.UTC),
				},
			},
		}

		expectedVersion := "v1.0.0"
		expectedConfig := &entity.ComponentConfiguration{}
		expectedConfig.ImageRepo = "tidbcloud/tikv"
		expectedConfig.ImageTag = "test-tag"
		expectedConfig.ConfigurationFileContent = `
concurrency = 16
custom-option = "allowed"
start-archive-duration = "2h"
data-dir = "/data"
expiration-date = "20251225"
`

		mockClusterConfigClient.EXPECT().GetBRComponentVersionAndConfig(ctx, targetSharedPool, tenantId, apiv1.ComponentType_TIBRGC_T3_ARCHIVE).
			Return(expectedVersion, expectedConfig, nil)

		image, options, err := action.getT3ImageAndOptionsFromConfigCenter(ctx, targetSharedPool, tenantId, backupGcSetting, 4)

		require.NoError(t, err)
		assert.Equal(t, "test-registry/test-repo/tikv:test-tag", image)
		assert.Equal(t, map[string]string{
			"concurrency":     "16",
			"custom-option":   "allowed",
			"expiration-date": "20251225",
			// start-archive-duration and data-dir should be removed
		}, options)
		assert.NotContains(t, options, "start-archive-duration")
		assert.NotContains(t, options, "data-dir")
	})
}
