package cron

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

	"github.com/samber/lo"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	apiv1beta "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	dplanemgrv1 "github.com/tidbcloud/cluster-service-ng/api/dataplane-manager/v1"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/pkg/dataplanemgr/mocks"
	"github.com/tidbcloud/cluster-service-ng/pkg/storage/mysql/schema"
	repomocks "github.com/tidbcloud/cluster-service-ng/pkg/storage/repo/mocks"
	"google.golang.org/protobuf/types/known/emptypb"
)

func TestCleanDataBucketJob_getToDeleteSharedPoolIds(t *testing.T) {
	tests := []struct {
		name           string
		gcSettings     []*entity.BackupGcSetting
		expectedResult []string
	}{
		{
			name: "auto backup expired",
			gcSettings: []*entity.BackupGcSetting{
				{
					BackupGcSetting: schema.BackupGcSetting{
						SharedPoolID:            "test-sharedpool-2",
						Status:                  schema.BackupGcStatusAutoBackupExpired,
						SharedPoolDeletedAt:     lo.ToPtr(time.Now().Add(-100 * 24 * time.Hour)), // 100 days ago
						HasManualBackup:         lo.ToPtr(false),
						MaxT1MetaLifetimeDays:   lo.ToPtr(uint32(7)),
						MaxT1WalLifetimeDays:    lo.ToPtr(uint32(7)),
						MaxT1PackedLifetimeDays: lo.ToPtr(uint32(4)),
						MaxT2SstLifetimeDays:    lo.ToPtr(uint32(30)),
						MaxT3DataLifetimeDays:   lo.ToPtr(uint32(90)),
					},
				},
			},
			expectedResult: []string{"test-sharedpool-2"},
		},
		{
			name: "has manual backup - should skip",
			gcSettings: []*entity.BackupGcSetting{
				{
					BackupGcSetting: schema.BackupGcSetting{
						SharedPoolID:            "test-sharedpool-4",
						Status:                  schema.BackupGcStatusAutoBackupExpired,
						SharedPoolDeletedAt:     lo.ToPtr(time.Now().Add(-100 * 24 * time.Hour)), // 100 days ago
						HasManualBackup:         lo.ToPtr(true),                                  // Has manual backup
						MaxT1MetaLifetimeDays:   lo.ToPtr(uint32(7)),
						MaxT1WalLifetimeDays:    lo.ToPtr(uint32(7)),
						MaxT1PackedLifetimeDays: lo.ToPtr(uint32(4)),
						MaxT2SstLifetimeDays:    lo.ToPtr(uint32(30)),
						MaxT3DataLifetimeDays:   lo.ToPtr(uint32(90)),
					},
				},
			},
			expectedResult: nil, // Should be nil because HasManualBackup is true
		},
		{
			name: "mixed scenarios",
			gcSettings: []*entity.BackupGcSetting{
				// Not expired - should skip (max retention is 90 days, deleted 1 day ago)
				{
					BackupGcSetting: schema.BackupGcSetting{
						SharedPoolID:            "test-sharedpool-6",
						Status:                  schema.BackupGcStatusAutoBackupExpired,
						SharedPoolDeletedAt:     lo.ToPtr(time.Now().Add(-24 * time.Hour)), // 1 day ago
						HasManualBackup:         lo.ToPtr(false),
						MaxT1MetaLifetimeDays:   lo.ToPtr(uint32(7)),
						MaxT1WalLifetimeDays:    lo.ToPtr(uint32(7)),
						MaxT1PackedLifetimeDays: lo.ToPtr(uint32(4)),
						MaxT2SstLifetimeDays:    lo.ToPtr(uint32(30)),
						MaxT3DataLifetimeDays:   lo.ToPtr(uint32(90)), // Max retention is 90 days
					},
				},
				// Expired, needs status update - should include
				{
					BackupGcSetting: schema.BackupGcSetting{
						SharedPoolID:            "test-sharedpool-7",
						Status:                  schema.BackupGcStatusAutoBackupExpired,
						SharedPoolDeletedAt:     lo.ToPtr(time.Now().Add(-100 * 24 * time.Hour)), // 100 days ago
						HasManualBackup:         lo.ToPtr(false),
						MaxT1MetaLifetimeDays:   lo.ToPtr(uint32(7)),
						MaxT1WalLifetimeDays:    lo.ToPtr(uint32(7)),
						MaxT1PackedLifetimeDays: lo.ToPtr(uint32(4)),
						MaxT2SstLifetimeDays:    lo.ToPtr(uint32(30)),
						MaxT3DataLifetimeDays:   lo.ToPtr(uint32(90)),
					},
				},
				// Already expired status - should include
				{
					BackupGcSetting: schema.BackupGcSetting{
						SharedPoolID:            "test-sharedpool-8",
						Status:                  schema.BackupGcStatusAutoBackupExpired,
						SharedPoolDeletedAt:     lo.ToPtr(time.Now().Add(-100 * 24 * time.Hour)), // 100 days ago
						HasManualBackup:         lo.ToPtr(false),
						MaxT1MetaLifetimeDays:   lo.ToPtr(uint32(7)),
						MaxT1WalLifetimeDays:    lo.ToPtr(uint32(7)),
						MaxT1PackedLifetimeDays: lo.ToPtr(uint32(4)),
						MaxT2SstLifetimeDays:    lo.ToPtr(uint32(30)),
						MaxT3DataLifetimeDays:   lo.ToPtr(uint32(90)),
					},
				},
				// Has manual backup - should skip
				{
					BackupGcSetting: schema.BackupGcSetting{
						SharedPoolID:            "test-sharedpool-9",
						Status:                  schema.BackupGcStatusAutoBackupExpired,
						SharedPoolDeletedAt:     lo.ToPtr(time.Now().Add(-100 * 24 * time.Hour)), // 100 days ago
						HasManualBackup:         lo.ToPtr(true),
						MaxT1MetaLifetimeDays:   lo.ToPtr(uint32(7)),
						MaxT1WalLifetimeDays:    lo.ToPtr(uint32(7)),
						MaxT1PackedLifetimeDays: lo.ToPtr(uint32(4)),
						MaxT2SstLifetimeDays:    lo.ToPtr(uint32(30)),
						MaxT3DataLifetimeDays:   lo.ToPtr(uint32(90)),
					},
				},
			},
			expectedResult: []string{"test-sharedpool-7", "test-sharedpool-8"}, // test-sharedpool-9 excluded due to manual backup
		},
	}

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

			// Create job instance
			job := &CleanDataBucketJob{}

			// Execute the function
			ctx := context.Background()
			result := job.getToDeleteSharedPoolIds(ctx, tt.gcSettings)

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

func TestCleanDataBucketJob_getToDeleteSharedPoolIds_UsesManualBackupExistenceAsSafetyGate(t *testing.T) {
	ctx := context.Background()
	gcSetting := &entity.BackupGcSetting{BackupGcSetting: schema.BackupGcSetting{
		SharedPoolID:            "sp-safety",
		Status:                  schema.BackupGcStatusAutoBackupExpired,
		SharedPoolDeletedAt:     lo.ToPtr(time.Now().Add(-100 * 24 * time.Hour)),
		HasManualBackup:         lo.ToPtr(false),
		MaxT1MetaLifetimeDays:   lo.ToPtr(uint32(7)),
		MaxT1WalLifetimeDays:    lo.ToPtr(uint32(7)),
		MaxT1PackedLifetimeDays: lo.ToPtr(uint32(4)),
		MaxT2SstLifetimeDays:    lo.ToPtr(uint32(30)),
		MaxT3DataLifetimeDays:   lo.ToPtr(uint32(90)),
	}}

	manualBackupRepo := repomocks.NewManualBackupRepo(t)
	gcRepo := repomocks.NewBackupGcSettingRepo(t)

	manualBackupRepo.EXPECT().ExistsUndeletedBySharedPool(mock.Anything, "sp-safety").Return(true, nil).Once()
	gcRepo.EXPECT().Save(mock.Anything, mock.MatchedBy(func(in *entity.BackupGcSetting) bool {
		return in.SharedPoolID == "sp-safety" && in.HasManualBackup != nil && *in.HasManualBackup
	})).Return(nil).Once()

	job := &CleanDataBucketJob{manualBackupRepo: manualBackupRepo, gcRepo: gcRepo}
	result := job.getToDeleteSharedPoolIds(ctx, []*entity.BackupGcSetting{gcSetting})
	assert.Nil(t, result)
}

func TestCleanDataBucketJob_doDelete(t *testing.T) {
	tests := []struct {
		name          string
		sharedPoolIds []string
		mockSetup     func(*repomocks.SharedPoolObjectStoreRepo, *repomocks.SharedPoolRepo, *repomocks.BackupGcSettingRepo, *mocks.Client)
	}{
		{
			name:          "successful deletion",
			sharedPoolIds: []string{"test-sharedpool-1"},
			mockSetup: func(objectStoreRepo *repomocks.SharedPoolObjectStoreRepo, sharedPoolRepo *repomocks.SharedPoolRepo, gcRepo *repomocks.BackupGcSettingRepo, bucketSvc *mocks.Client) {
				// Mock bucket info load
				objectStoreRepo.EXPECT().Load(mock.Anything, "test-sharedpool-1", schema.SharedPoolObjectStoreTypeData, true).Return(&entity.SharedPoolObjectStore{
					SharedPoolObjectStore: schema.SharedPoolObjectStore{
						StoreBucket: "test-bucket-1",
						StoreRegion: "us-west-2",
					},
				}, nil)

				// Mock shared pool load
				sharedPoolRepo.EXPECT().Load(mock.Anything, "test-sharedpool-1", true).Return(&entity.SharedPool{
					SharedPool: apiv1beta.SharedPool{
						DataplaneName: "test-dataplane",
					},
				}, nil)

				// Mock bucket get
				bucketSvc.EXPECT().GetBucket(mock.Anything, &dplanemgrv1.GetBucketRequest{
					DataplaneId: "test-dataplane",
					BucketId:    "test-bucket-1",
				}).Return(&dplanemgrv1.Bucket{}, nil)

				// Mock bucket deletion
				bucketSvc.EXPECT().DeleteBucket(mock.Anything, &dplanemgrv1.DeleteBucketRequest{
					DataplaneId: "test-dataplane",
					BucketId:    "test-bucket-1",
				}).Return(&emptypb.Empty{}, nil) // Mock object store repo deletion
				objectStoreRepo.EXPECT().Delete(mock.Anything, "test-sharedpool-1", schema.SharedPoolObjectStoreTypeData).Return(nil)

				// Mock GC setting deletion
				gcRepo.EXPECT().Delete(mock.Anything, "test-sharedpool-1").Return(nil)
			},
		},
		{
			name:          "bucket info load error",
			sharedPoolIds: []string{"test-sharedpool-2"},
			mockSetup: func(objectStoreRepo *repomocks.SharedPoolObjectStoreRepo, sharedPoolRepo *repomocks.SharedPoolRepo, gcRepo *repomocks.BackupGcSettingRepo, bucketSvc *mocks.Client) {
				// Mock bucket info load failure
				objectStoreRepo.EXPECT().Load(mock.Anything, "test-sharedpool-2", schema.SharedPoolObjectStoreTypeData, true).Return(nil, errors.New("bucket info not found"))
			},
		},
		{
			name:          "shared pool load error",
			sharedPoolIds: []string{"test-sharedpool-3"},
			mockSetup: func(objectStoreRepo *repomocks.SharedPoolObjectStoreRepo, sharedPoolRepo *repomocks.SharedPoolRepo, gcRepo *repomocks.BackupGcSettingRepo, bucketSvc *mocks.Client) {
				// Mock bucket info load success
				objectStoreRepo.EXPECT().Load(mock.Anything, "test-sharedpool-3", schema.SharedPoolObjectStoreTypeData, true).Return(&entity.SharedPoolObjectStore{
					SharedPoolObjectStore: schema.SharedPoolObjectStore{
						StoreBucket: "test-bucket-3",
						StoreRegion: "us-west-2",
					},
				}, nil)

				// Mock shared pool load failure
				sharedPoolRepo.EXPECT().Load(mock.Anything, "test-sharedpool-3", true).Return(nil, errors.New("shared pool not found"))
			},
		},
		{
			name:          "bucket deletion error",
			sharedPoolIds: []string{"test-sharedpool-4"},
			mockSetup: func(objectStoreRepo *repomocks.SharedPoolObjectStoreRepo, sharedPoolRepo *repomocks.SharedPoolRepo, gcRepo *repomocks.BackupGcSettingRepo, bucketSvc *mocks.Client) {
				// Mock bucket info load success
				objectStoreRepo.EXPECT().Load(mock.Anything, "test-sharedpool-4", schema.SharedPoolObjectStoreTypeData, true).Return(&entity.SharedPoolObjectStore{
					SharedPoolObjectStore: schema.SharedPoolObjectStore{
						StoreBucket: "test-bucket-4",
						StoreRegion: "us-west-2",
					},
				}, nil)

				// Mock shared pool load success
				sharedPoolRepo.EXPECT().Load(mock.Anything, "test-sharedpool-4", true).Return(&entity.SharedPool{
					SharedPool: apiv1beta.SharedPool{
						DataplaneName: "test-dataplane",
					},
				}, nil)

				// Mock bucket get
				bucketSvc.EXPECT().GetBucket(mock.Anything, &dplanemgrv1.GetBucketRequest{
					DataplaneId: "test-dataplane",
					BucketId:    "test-bucket-4",
				}).Return(&dplanemgrv1.Bucket{}, nil)

				// Mock bucket deletion failure
				bucketSvc.EXPECT().DeleteBucket(mock.Anything, &dplanemgrv1.DeleteBucketRequest{
					DataplaneId: "test-dataplane",
					BucketId:    "test-bucket-4",
				}).Return(nil, errors.New("bucket deletion failed"))
			},
		},
		{
			name:          "gc setting deletion error",
			sharedPoolIds: []string{"test-sharedpool-5"},
			mockSetup: func(objectStoreRepo *repomocks.SharedPoolObjectStoreRepo, sharedPoolRepo *repomocks.SharedPoolRepo, gcRepo *repomocks.BackupGcSettingRepo, bucketSvc *mocks.Client) {
				// Mock bucket info load success
				objectStoreRepo.EXPECT().Load(mock.Anything, "test-sharedpool-5", schema.SharedPoolObjectStoreTypeData, true).Return(&entity.SharedPoolObjectStore{
					SharedPoolObjectStore: schema.SharedPoolObjectStore{
						StoreBucket: "test-bucket-5",
						StoreRegion: "us-west-2",
					},
				}, nil)

				// Mock shared pool load success
				sharedPoolRepo.EXPECT().Load(mock.Anything, "test-sharedpool-5", true).Return(&entity.SharedPool{
					SharedPool: apiv1beta.SharedPool{
						DataplaneName: "test-dataplane",
					},
				}, nil)

				// Mock bucket get
				bucketSvc.EXPECT().GetBucket(mock.Anything, &dplanemgrv1.GetBucketRequest{
					DataplaneId: "test-dataplane",
					BucketId:    "test-bucket-5",
				}).Return(&dplanemgrv1.Bucket{}, nil)

				// Mock bucket deletion success
				bucketSvc.EXPECT().DeleteBucket(mock.Anything, &dplanemgrv1.DeleteBucketRequest{
					DataplaneId: "test-dataplane",
					BucketId:    "test-bucket-5",
				}).Return(&emptypb.Empty{}, nil) // Mock object store repo deletion
				objectStoreRepo.EXPECT().Delete(mock.Anything, "test-sharedpool-5", schema.SharedPoolObjectStoreTypeData).Return(nil)

				// Mock GC setting deletion failure
				gcRepo.EXPECT().Delete(mock.Anything, "test-sharedpool-5").Return(errors.New("gc setting deletion failed"))
			},
		},
		{
			name:          "object store deletion error",
			sharedPoolIds: []string{"test-sharedpool-obj-error"},
			mockSetup: func(objectStoreRepo *repomocks.SharedPoolObjectStoreRepo, sharedPoolRepo *repomocks.SharedPoolRepo, gcRepo *repomocks.BackupGcSettingRepo, bucketSvc *mocks.Client) {
				// Mock bucket info load success
				objectStoreRepo.EXPECT().Load(mock.Anything, "test-sharedpool-obj-error", schema.SharedPoolObjectStoreTypeData, true).Return(&entity.SharedPoolObjectStore{
					SharedPoolObjectStore: schema.SharedPoolObjectStore{
						StoreBucket: "test-bucket-obj-error",
						StoreRegion: "us-west-2",
					},
				}, nil)

				// Mock shared pool load success
				sharedPoolRepo.EXPECT().Load(mock.Anything, "test-sharedpool-obj-error", true).Return(&entity.SharedPool{
					SharedPool: apiv1beta.SharedPool{
						DataplaneName: "test-dataplane",
					},
				}, nil)

				// Mock bucket get
				bucketSvc.EXPECT().GetBucket(mock.Anything, &dplanemgrv1.GetBucketRequest{
					DataplaneId: "test-dataplane",
					BucketId:    "test-bucket-obj-error",
				}).Return(&dplanemgrv1.Bucket{}, nil)

				// Mock bucket deletion success
				bucketSvc.EXPECT().DeleteBucket(mock.Anything, &dplanemgrv1.DeleteBucketRequest{
					DataplaneId: "test-dataplane",
					BucketId:    "test-bucket-obj-error",
				}).Return(&emptypb.Empty{}, nil) // Mock object store repo deletion failure
				objectStoreRepo.EXPECT().Delete(mock.Anything, "test-sharedpool-obj-error", schema.SharedPoolObjectStoreTypeData).Return(errors.New("object store deletion failed"))
			},
		},
		{
			name:          "multiple shared pools - all successful",
			sharedPoolIds: []string{"test-sharedpool-6", "test-sharedpool-7"},
			mockSetup: func(objectStoreRepo *repomocks.SharedPoolObjectStoreRepo, sharedPoolRepo *repomocks.SharedPoolRepo, gcRepo *repomocks.BackupGcSettingRepo, bucketSvc *mocks.Client) {
				// Mock for test-sharedpool-6
				objectStoreRepo.EXPECT().Load(mock.Anything, "test-sharedpool-6", schema.SharedPoolObjectStoreTypeData, true).Return(&entity.SharedPoolObjectStore{
					SharedPoolObjectStore: schema.SharedPoolObjectStore{
						StoreBucket: "test-bucket-6",
						StoreRegion: "us-west-2",
					},
				}, nil)
				sharedPoolRepo.EXPECT().Load(mock.Anything, "test-sharedpool-6", true).Return(&entity.SharedPool{
					SharedPool: apiv1beta.SharedPool{
						DataplaneName: "test-dataplane",
					},
				}, nil)
				bucketSvc.EXPECT().GetBucket(mock.Anything, &dplanemgrv1.GetBucketRequest{
					DataplaneId: "test-dataplane",
					BucketId:    "test-bucket-6",
				}).Return(&dplanemgrv1.Bucket{}, nil)
				bucketSvc.EXPECT().DeleteBucket(mock.Anything, &dplanemgrv1.DeleteBucketRequest{
					DataplaneId: "test-dataplane",
					BucketId:    "test-bucket-6",
				}).Return(&emptypb.Empty{}, nil)
				objectStoreRepo.EXPECT().Delete(mock.Anything, "test-sharedpool-6", schema.SharedPoolObjectStoreTypeData).Return(nil)
				gcRepo.EXPECT().Delete(mock.Anything, "test-sharedpool-6").Return(nil)

				// Mock for test-sharedpool-7
				objectStoreRepo.EXPECT().Load(mock.Anything, "test-sharedpool-7", schema.SharedPoolObjectStoreTypeData, true).Return(&entity.SharedPoolObjectStore{
					SharedPoolObjectStore: schema.SharedPoolObjectStore{
						StoreBucket: "test-bucket-7",
						StoreRegion: "us-west-2",
					},
				}, nil)
				sharedPoolRepo.EXPECT().Load(mock.Anything, "test-sharedpool-7", true).Return(&entity.SharedPool{
					SharedPool: apiv1beta.SharedPool{
						DataplaneName: "test-dataplane",
					},
				}, nil)
				bucketSvc.EXPECT().GetBucket(mock.Anything, &dplanemgrv1.GetBucketRequest{
					DataplaneId: "test-dataplane",
					BucketId:    "test-bucket-7",
				}).Return(&dplanemgrv1.Bucket{}, nil)
				bucketSvc.EXPECT().DeleteBucket(mock.Anything, &dplanemgrv1.DeleteBucketRequest{
					DataplaneId: "test-dataplane",
					BucketId:    "test-bucket-7",
				}).Return(&emptypb.Empty{}, nil)
				objectStoreRepo.EXPECT().Delete(mock.Anything, "test-sharedpool-7", schema.SharedPoolObjectStoreTypeData).Return(nil)
				gcRepo.EXPECT().Delete(mock.Anything, "test-sharedpool-7").Return(nil)
			},
		},
		{
			name:          "multiple shared pools - mixed success and failure",
			sharedPoolIds: []string{"test-sharedpool-8", "test-sharedpool-9"},
			mockSetup: func(objectStoreRepo *repomocks.SharedPoolObjectStoreRepo, sharedPoolRepo *repomocks.SharedPoolRepo, gcRepo *repomocks.BackupGcSettingRepo, bucketSvc *mocks.Client) {
				// Mock for test-sharedpool-8 - success
				objectStoreRepo.EXPECT().Load(mock.Anything, "test-sharedpool-8", schema.SharedPoolObjectStoreTypeData, true).Return(&entity.SharedPoolObjectStore{
					SharedPoolObjectStore: schema.SharedPoolObjectStore{
						StoreBucket: "test-bucket-8",
						StoreRegion: "us-west-2",
					},
				}, nil)
				sharedPoolRepo.EXPECT().Load(mock.Anything, "test-sharedpool-8", true).Return(&entity.SharedPool{
					SharedPool: apiv1beta.SharedPool{
						DataplaneName: "test-dataplane",
					},
				}, nil)
				bucketSvc.EXPECT().GetBucket(mock.Anything, &dplanemgrv1.GetBucketRequest{
					DataplaneId: "test-dataplane",
					BucketId:    "test-bucket-8",
				}).Return(&dplanemgrv1.Bucket{}, nil)
				bucketSvc.EXPECT().DeleteBucket(mock.Anything, &dplanemgrv1.DeleteBucketRequest{
					DataplaneId: "test-dataplane",
					BucketId:    "test-bucket-8",
				}).Return(&emptypb.Empty{}, nil)
				objectStoreRepo.EXPECT().Delete(mock.Anything, "test-sharedpool-8", schema.SharedPoolObjectStoreTypeData).Return(nil)
				gcRepo.EXPECT().Delete(mock.Anything, "test-sharedpool-8").Return(nil)

				// Mock for test-sharedpool-9 - bucket info load failure
				objectStoreRepo.EXPECT().Load(mock.Anything, "test-sharedpool-9", schema.SharedPoolObjectStoreTypeData, true).Return(nil, errors.New("bucket info not found"))
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create mock repositories
			mockObjectStoreRepo := repomocks.NewSharedPoolObjectStoreRepo(t)
			mockSharedPoolRepo := repomocks.NewSharedPoolRepo(t)
			mockGcRepo := repomocks.NewBackupGcSettingRepo(t)
			mockBucketSvc := mocks.NewClient(t)

			// Setup mock expectations
			tt.mockSetup(mockObjectStoreRepo, mockSharedPoolRepo, mockGcRepo, mockBucketSvc)

			// Create job instance
			job := &CleanDataBucketJob{
				sharedPoolObjectStoreRepo: mockObjectStoreRepo,
				sharedPoolRepo:            mockSharedPoolRepo,
				gcRepo:                    mockGcRepo,
				bucketSvc:                 mockBucketSvc,
			}

			// Execute the function
			ctx := context.Background()
			job.doDelete(ctx, tt.sharedPoolIds)

			// Verify all mock expectations were met
			mockObjectStoreRepo.AssertExpectations(t)
			mockSharedPoolRepo.AssertExpectations(t)
			mockGcRepo.AssertExpectations(t)
			mockBucketSvc.AssertExpectations(t)
		})
	}
}
