package br

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/mock/gomock"

	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/br"
	mockbrrepo "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/br/mock_br"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/infra/repoimpl/schema"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/pkg/config"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

func TestExpireBackups(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	ctx := t.Context()

	t.Run("disabled - skip execution", func(t *testing.T) {
		mockBackupSettingRepo := mockbrrepo.NewMockBackupSettingRepo(ctrl)
		mockBackupRepo := mockbrrepo.NewMockBackupRepo(ctrl)

		worker := &worker{
			backupSettingRepo: mockBackupSettingRepo,
			backupRepo:        mockBackupRepo,
			cfg: &config.BackupConfig{
				ExpireConfig: config.ExpireBackupsConfig{
					Enable: false,
				},
			},
		}

		err := worker.ExpireBackups(ctx)
		assert.NoError(t, err)
	})

	t.Run("success - no backup settings to expire", func(t *testing.T) {
		mockBackupSettingRepo := mockbrrepo.NewMockBackupSettingRepo(ctrl)
		mockBackupRepo := mockbrrepo.NewMockBackupRepo(ctrl)

		worker := &worker{
			backupSettingRepo: mockBackupSettingRepo,
			backupRepo:        mockBackupRepo,
			cfg: &config.BackupConfig{
				ExpireConfig: config.ExpireBackupsConfig{
					Enable: true,
				},
			},
		}

		// Mock query returns empty list
		mockBackupSettingRepo.EXPECT().UnscopedQuery(ctx, &br.QueryBackupSettingFilter{
			ExcludeStates: schema.BackupSettingStatesClusterDeletedNoAutoBackups,
		}, nil).Return([]*entity.BackupSetting{}, nil)

		err := worker.ExpireBackups(ctx)
		assert.NoError(t, err)
	})

	t.Run("success - backup settings processed successfully", func(t *testing.T) {
		mockBackupSettingRepo := mockbrrepo.NewMockBackupSettingRepo(ctrl)
		mockBackupRepo := mockbrrepo.NewMockBackupRepo(ctrl)

		worker := &worker{
			backupSettingRepo: mockBackupSettingRepo,
			backupRepo:        mockBackupRepo,
			cfg: &config.BackupConfig{
				ExpireConfig: config.ExpireBackupsConfig{
					Enable: true,
				},
			},
		}

		now := time.Now().UTC()
		backupSettings := []*entity.BackupSetting{
			{
				BackupSetting: &schema.BackupSetting{
					ID:        1,
					ClusterId: 100,
					State:     schema.BackupSettingStateCreated,
					Schedule: schema.AutoSchedule{
						Type: "per-day",
						At:   14 * 60,
					},
					LastCleanAt: &now, // Will be skipped due to recent clean
				},
			},
			{
				BackupSetting: &schema.BackupSetting{
					ID:        2,
					ClusterId: 200,
					State:     schema.BackupSettingStateCreated,
					Schedule: schema.AutoSchedule{
						Type: "per-hour",
						At:   30,
					},
					LastCleanAt: nil, // Will be processed
				},
			},
		}

		// Mock query returns backup settings
		mockBackupSettingRepo.EXPECT().UnscopedQuery(ctx, &br.QueryBackupSettingFilter{
			ExcludeStates: schema.BackupSettingStatesClusterDeletedNoAutoBackups,
		}, nil).Return(backupSettings, nil)

		// Mock delete for cluster 200 (cluster 100 will be skipped)
		mockBackupRepo.EXPECT().Delete(ctx, gomock.Any()).DoAndReturn(
			func(ctx context.Context, filter *br.DeleteBackupsFilter) bizerr.BizError {
				assert.Equal(t, []uint64{200}, filter.ClusterIds)
				assert.Equal(t, []schema.BackupTriggerType{schema.BackupTriggerTypeAuto}, filter.TriggerTypes)
				assert.NotNil(t, filter.ExpirationTimeLte)
				return nil
			},
		)

		// Mock save for cluster 200
		mockBackupSettingRepo.EXPECT().Save(ctx, gomock.Any()).DoAndReturn(
			func(ctx context.Context, setting *entity.BackupSetting) bizerr.BizError {
				assert.Equal(t, uint64(200), setting.ClusterId)
				assert.NotNil(t, setting.LastCleanAt)
				return nil
			},
		)

		// Mock state transition for cluster 200
		mockBackupSettingRepo.EXPECT().TransitStateAfterBackupDeletedIfNeeded(ctx, uint64(200)).Return(nil)

		err := worker.ExpireBackups(ctx)
		assert.NoError(t, err)
	})

	t.Run("success - multiple backup settings processed", func(t *testing.T) {
		mockBackupSettingRepo := mockbrrepo.NewMockBackupSettingRepo(ctrl)
		mockBackupRepo := mockbrrepo.NewMockBackupRepo(ctrl)

		worker := &worker{
			backupSettingRepo: mockBackupSettingRepo,
			backupRepo:        mockBackupRepo,
			cfg: &config.BackupConfig{
				ExpireConfig: config.ExpireBackupsConfig{
					Enable: true,
				},
			},
		}

		backupSettings := []*entity.BackupSetting{
			{
				BackupSetting: &schema.BackupSetting{
					ID:        1,
					ClusterId: 100,
					State:     schema.BackupSettingStateCreated,
					Schedule: schema.AutoSchedule{
						Type: "per-day",
						At:   14 * 60,
					},
					LastCleanAt: nil,
				},
			},
			{
				BackupSetting: &schema.BackupSetting{
					ID:        2,
					ClusterId: 200,
					State:     schema.BackupSettingStateCreated,
					Schedule: schema.AutoSchedule{
						Type: "per-hour",
						At:   30,
					},
					LastCleanAt: nil,
				},
			},
		}

		// Mock query returns backup settings
		mockBackupSettingRepo.EXPECT().UnscopedQuery(ctx, &br.QueryBackupSettingFilter{
			ExcludeStates: schema.BackupSettingStatesClusterDeletedNoAutoBackups,
		}, nil).Return(backupSettings, nil)

		// Mock delete for cluster 100
		mockBackupRepo.EXPECT().Delete(ctx, gomock.Any()).DoAndReturn(
			func(ctx context.Context, filter *br.DeleteBackupsFilter) bizerr.BizError {
				assert.Equal(t, []uint64{100}, filter.ClusterIds)
				assert.Equal(t, []schema.BackupTriggerType{schema.BackupTriggerTypeAuto}, filter.TriggerTypes)
				assert.NotNil(t, filter.ExpirationTimeLte)
				return nil
			},
		)

		// Mock save for cluster 100
		mockBackupSettingRepo.EXPECT().Save(ctx, gomock.Any()).DoAndReturn(
			func(ctx context.Context, setting *entity.BackupSetting) bizerr.BizError {
				assert.Equal(t, uint64(100), setting.ClusterId)
				assert.NotNil(t, setting.LastCleanAt)
				return nil
			},
		)

		// Mock state transition for cluster 100
		mockBackupSettingRepo.EXPECT().TransitStateAfterBackupDeletedIfNeeded(ctx, uint64(100)).Return(nil)

		// Mock delete for cluster 200
		mockBackupRepo.EXPECT().Delete(ctx, gomock.Any()).DoAndReturn(
			func(ctx context.Context, filter *br.DeleteBackupsFilter) bizerr.BizError {
				assert.Equal(t, []uint64{200}, filter.ClusterIds)
				assert.Equal(t, []schema.BackupTriggerType{schema.BackupTriggerTypeAuto}, filter.TriggerTypes)
				assert.NotNil(t, filter.ExpirationTimeLte)
				return nil
			},
		)

		// Mock save for cluster 200
		mockBackupSettingRepo.EXPECT().Save(ctx, gomock.Any()).DoAndReturn(
			func(ctx context.Context, setting *entity.BackupSetting) bizerr.BizError {
				assert.Equal(t, uint64(200), setting.ClusterId)
				assert.NotNil(t, setting.LastCleanAt)
				return nil
			},
		)

		// Mock state transition for cluster 200
		mockBackupSettingRepo.EXPECT().TransitStateAfterBackupDeletedIfNeeded(ctx, uint64(200)).Return(nil)

		err := worker.ExpireBackups(ctx)
		assert.NoError(t, err)
	})

	t.Run("error - query backup settings fails", func(t *testing.T) {
		mockBackupSettingRepo := mockbrrepo.NewMockBackupSettingRepo(ctrl)
		mockBackupRepo := mockbrrepo.NewMockBackupRepo(ctrl)

		worker := &worker{
			backupSettingRepo: mockBackupSettingRepo,
			backupRepo:        mockBackupRepo,
			cfg: &config.BackupConfig{
				ExpireConfig: config.ExpireBackupsConfig{
					Enable: true,
				},
			},
		}

		expectedErr := bizerr.New(bizerr.ErrCodeDB, "database error")
		mockBackupSettingRepo.EXPECT().UnscopedQuery(ctx, &br.QueryBackupSettingFilter{
			ExcludeStates: schema.BackupSettingStatesClusterDeletedNoAutoBackups,
		}, nil).Return(nil, expectedErr)

		err := worker.ExpireBackups(ctx)
		require.Error(t, err)
		assert.Contains(t, err.Error(), "failed to query non-cleanup backup settings")
	})

	t.Run("error - delete expired backups fails", func(t *testing.T) {
		mockBackupSettingRepo := mockbrrepo.NewMockBackupSettingRepo(ctrl)
		mockBackupRepo := mockbrrepo.NewMockBackupRepo(ctrl)

		worker := &worker{
			backupSettingRepo: mockBackupSettingRepo,
			backupRepo:        mockBackupRepo,
			cfg: &config.BackupConfig{
				ExpireConfig: config.ExpireBackupsConfig{
					Enable: true,
				},
			},
		}

		backupSettings := []*entity.BackupSetting{
			{
				BackupSetting: &schema.BackupSetting{
					ID:        1,
					ClusterId: 100,
					State:     schema.BackupSettingStateCreated,
					Schedule: schema.AutoSchedule{
						Type: "per-day",
						At:   14 * 60,
					},
					LastCleanAt: nil,
				},
			},
		}

		// Mock query returns backup settings
		mockBackupSettingRepo.EXPECT().UnscopedQuery(ctx, &br.QueryBackupSettingFilter{
			ExcludeStates: schema.BackupSettingStatesClusterDeletedNoAutoBackups,
		}, nil).Return(backupSettings, nil)

		// Mock delete fails
		expectedErr := bizerr.New(bizerr.ErrCodeDB, "delete failed")
		mockBackupRepo.EXPECT().Delete(ctx, gomock.Any()).Return(expectedErr)

		err := worker.ExpireBackups(ctx)
		require.NoError(t, err)
	})

	t.Run("error - save backup setting fails", func(t *testing.T) {
		mockBackupSettingRepo := mockbrrepo.NewMockBackupSettingRepo(ctrl)
		mockBackupRepo := mockbrrepo.NewMockBackupRepo(ctrl)

		worker := &worker{
			backupSettingRepo: mockBackupSettingRepo,
			backupRepo:        mockBackupRepo,
			cfg: &config.BackupConfig{
				ExpireConfig: config.ExpireBackupsConfig{
					Enable: true,
				},
			},
		}

		backupSettings := []*entity.BackupSetting{
			{
				BackupSetting: &schema.BackupSetting{
					ID:        1,
					ClusterId: 100,
					State:     schema.BackupSettingStateCreated,
					Schedule: schema.AutoSchedule{
						Type: "per-day",
						At:   14 * 60,
					},
					LastCleanAt: nil,
				},
			},
		}

		// Mock query returns backup settings
		mockBackupSettingRepo.EXPECT().UnscopedQuery(ctx, &br.QueryBackupSettingFilter{
			ExcludeStates: schema.BackupSettingStatesClusterDeletedNoAutoBackups,
		}, nil).Return(backupSettings, nil)

		// Mock delete succeeds
		mockBackupRepo.EXPECT().Delete(ctx, gomock.Any()).Return(nil)

		// Mock save fails
		expectedErr := bizerr.New(bizerr.ErrCodeDB, "save failed")
		mockBackupSettingRepo.EXPECT().Save(ctx, gomock.Any()).Return(expectedErr)

		err := worker.ExpireBackups(ctx)
		require.NoError(t, err)
	})

	t.Run("error - transit state fails", func(t *testing.T) {
		mockBackupSettingRepo := mockbrrepo.NewMockBackupSettingRepo(ctrl)
		mockBackupRepo := mockbrrepo.NewMockBackupRepo(ctrl)

		worker := &worker{
			backupSettingRepo: mockBackupSettingRepo,
			backupRepo:        mockBackupRepo,
			cfg: &config.BackupConfig{
				ExpireConfig: config.ExpireBackupsConfig{
					Enable: true,
				},
			},
		}

		backupSettings := []*entity.BackupSetting{
			{
				BackupSetting: &schema.BackupSetting{
					ID:        1,
					ClusterId: 100,
					State:     schema.BackupSettingStateCreated,
					Schedule: schema.AutoSchedule{
						Type: "per-day",
						At:   14 * 60,
					},
					LastCleanAt: nil,
				},
			},
		}

		// Mock query returns backup settings
		mockBackupSettingRepo.EXPECT().UnscopedQuery(ctx, &br.QueryBackupSettingFilter{
			ExcludeStates: schema.BackupSettingStatesClusterDeletedNoAutoBackups,
		}, nil).Return(backupSettings, nil)

		// Mock delete succeeds
		mockBackupRepo.EXPECT().Delete(ctx, gomock.Any()).Return(nil)

		// Mock save succeeds
		mockBackupSettingRepo.EXPECT().Save(ctx, gomock.Any()).Return(nil)

		// Mock state transition fails
		expectedErr := bizerr.New(bizerr.ErrCodeDB, "transit failed")
		mockBackupSettingRepo.EXPECT().TransitStateAfterBackupDeletedIfNeeded(ctx, uint64(100)).Return(expectedErr)

		err := worker.ExpireBackups(ctx)
		require.NoError(t, err)
	})
}

func TestHasCleanRecently(t *testing.T) {
	now := time.Date(2025, 1, 15, 14, 0, 0, 0, time.UTC)

	t.Run("nil LastCleanAt returns false", func(t *testing.T) {
		backupSetting := &entity.BackupSetting{
			BackupSetting: &schema.BackupSetting{
				LastCleanAt: nil,
				Schedule: schema.AutoSchedule{
					Type: "per-day",
				},
			},
		}

		result := hasCleanRecently(backupSetting, &now)
		assert.False(t, result)
	})

	t.Run("per-day schedule - cleaned within 24 hours", func(t *testing.T) {
		lastCleanAt := now.Add(-12 * time.Hour) // 12 hours ago
		backupSetting := &entity.BackupSetting{
			BackupSetting: &schema.BackupSetting{
				LastCleanAt: &lastCleanAt,
				Schedule: schema.AutoSchedule{
					Type: "per-day",
				},
			},
		}

		result := hasCleanRecently(backupSetting, &now)
		assert.True(t, result)
	})

	t.Run("per-day schedule - cleaned more than 24 hours ago", func(t *testing.T) {
		lastCleanAt := now.Add(-25 * time.Hour) // 25 hours ago
		backupSetting := &entity.BackupSetting{
			BackupSetting: &schema.BackupSetting{
				LastCleanAt: &lastCleanAt,
				Schedule: schema.AutoSchedule{
					Type: "per-day",
				},
			},
		}

		result := hasCleanRecently(backupSetting, &now)
		assert.False(t, result)
	})

	t.Run("per-hour schedule - cleaned within 1 hour", func(t *testing.T) {
		lastCleanAt := now.Add(-30 * time.Minute) // 30 minutes ago
		backupSetting := &entity.BackupSetting{
			BackupSetting: &schema.BackupSetting{
				LastCleanAt: &lastCleanAt,
				Schedule: schema.AutoSchedule{
					Type: "per-hour",
				},
			},
		}

		result := hasCleanRecently(backupSetting, &now)
		assert.True(t, result)
	})

	t.Run("per-hour schedule - cleaned more than 1 hour ago", func(t *testing.T) {
		lastCleanAt := now.Add(-90 * time.Minute) // 90 minutes ago
		backupSetting := &entity.BackupSetting{
			BackupSetting: &schema.BackupSetting{
				LastCleanAt: &lastCleanAt,
				Schedule: schema.AutoSchedule{
					Type: "per-hour",
				},
			},
		}

		result := hasCleanRecently(backupSetting, &now)
		assert.False(t, result)
	})

	t.Run("per-minute schedule - cleaned within 1 hour", func(t *testing.T) {
		lastCleanAt := now.Add(-30 * time.Minute) // 30 minutes ago
		backupSetting := &entity.BackupSetting{
			BackupSetting: &schema.BackupSetting{
				LastCleanAt: &lastCleanAt,
				Schedule: schema.AutoSchedule{
					Type: "per-minute",
				},
			},
		}

		result := hasCleanRecently(backupSetting, &now)
		assert.True(t, result)
	})

	t.Run("per-minute schedule - cleaned more than 1 hour ago", func(t *testing.T) {
		lastCleanAt := now.Add(-90 * time.Minute) // 90 minutes ago
		backupSetting := &entity.BackupSetting{
			BackupSetting: &schema.BackupSetting{
				LastCleanAt: &lastCleanAt,
				Schedule: schema.AutoSchedule{
					Type: "per-minute",
				},
			},
		}

		result := hasCleanRecently(backupSetting, &now)
		assert.False(t, result)
	})

	t.Run("invalid schedule type panics", func(t *testing.T) {
		lastCleanAt := now.Add(-1 * time.Hour)
		backupSetting := &entity.BackupSetting{
			BackupSetting: &schema.BackupSetting{
				LastCleanAt: &lastCleanAt,
				Schedule: schema.AutoSchedule{
					Type: "invalid",
				},
			},
		}

		assert.Panics(t, func() {
			hasCleanRecently(backupSetting, &now)
		})
	})
}

func TestExpireBackupsDuringPausePreparationDeletesOnlyAutomaticBackups(t *testing.T) {
	ctrl := gomock.NewController(t)
	backupSettingRepo := mockbrrepo.NewMockBackupSettingRepo(ctrl)
	backupRepo := mockbrrepo.NewMockBackupRepo(ctrl)
	setting := &entity.BackupSetting{BackupSetting: &schema.BackupSetting{
		ClusterId:    100,
		SharedPoolId: 42,
		State:        schema.BackupSettingStateCreated,
		Schedule:     schema.AutoSchedule{Type: "per-day"},
	}}
	worker := &worker{
		backupSettingRepo: backupSettingRepo,
		backupRepo:        backupRepo,
		cfg: &config.BackupConfig{ExpireConfig: config.ExpireBackupsConfig{
			Enable: true,
		}},
		pauseFencer: func(context.Context, uint64, func(context.Context) error) (bool, error) {
			t.Fatal("automatic-backup retention must not take the Pause preparation fence")
			return false, nil
		},
	}

	backupSettingRepo.EXPECT().UnscopedQuery(gomock.Any(), &br.QueryBackupSettingFilter{
		ExcludeStates: schema.BackupSettingStatesClusterDeletedNoAutoBackups,
	}, nil).Return([]*entity.BackupSetting{setting}, nil)
	backupRepo.EXPECT().Delete(gomock.Any(), gomock.Any()).DoAndReturn(
		func(_ context.Context, filter *br.DeleteBackupsFilter) bizerr.BizError {
			require.Equal(t, []uint64{100}, filter.ClusterIds)
			require.Equal(t, []schema.BackupTriggerType{schema.BackupTriggerTypeAuto}, filter.TriggerTypes)
			return nil
		},
	)
	backupSettingRepo.EXPECT().Save(gomock.Any(), setting).Return(nil)
	backupSettingRepo.EXPECT().TransitStateAfterBackupDeletedIfNeeded(gomock.Any(), uint64(100)).Return(nil)

	require.NoError(t, worker.ExpireBackups(t.Context()))
}
