package cron

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

	"github.com/samber/lo"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	apiv1 "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"
	dpmgrmocks "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 TestCleanExpiredAutoBackupDataJob_Execute_ListError(t *testing.T) {
	ctx := context.Background()

	mockGcRepo := repomocks.NewBackupGcSettingRepo(t)
	mockSharedPoolRepo := repomocks.NewSharedPoolRepo(t)
	mockObjectStoreRepo := repomocks.NewSharedPoolObjectStoreRepo(t)
	mockBucketSvc := dpmgrmocks.NewClient(t)

	job := NewCleanExpiredAutoBackupDataJob("@daily", mockGcRepo, mockSharedPoolRepo, mockObjectStoreRepo, mockBucketSvc)

	expectedErr := errors.New("list error")
	mockGcRepo.EXPECT().List(mock.Anything, mock.AnythingOfType("*repo.BackupGcSettingFilter")).Return(nil, expectedErr)

	err := job.Execute(ctx)
	require.ErrorIs(t, err, expectedErr)
}

func TestCleanExpiredAutoBackupDataJob_Execute_NoSettings(t *testing.T) {
	ctx := context.Background()

	mockGcRepo := repomocks.NewBackupGcSettingRepo(t)
	mockSharedPoolRepo := repomocks.NewSharedPoolRepo(t)
	mockObjectStoreRepo := repomocks.NewSharedPoolObjectStoreRepo(t)
	mockBucketSvc := dpmgrmocks.NewClient(t)

	job := NewCleanExpiredAutoBackupDataJob("@daily", mockGcRepo, mockSharedPoolRepo, mockObjectStoreRepo, mockBucketSvc)

	mockGcRepo.EXPECT().List(mock.Anything, mock.AnythingOfType("*repo.BackupGcSettingFilter")).Return([]*entity.BackupGcSetting{}, nil)

	err := job.Execute(ctx)
	require.NoError(t, err)
}

func TestCleanExpiredAutoBackupDataJob_Execute_SkipNotExpired(t *testing.T) {
	ctx := context.Background()

	mockGcRepo := repomocks.NewBackupGcSettingRepo(t)
	mockSharedPoolRepo := repomocks.NewSharedPoolRepo(t)
	mockObjectStoreRepo := repomocks.NewSharedPoolObjectStoreRepo(t)
	mockBucketSvc := dpmgrmocks.NewClient(t)

	job := NewCleanExpiredAutoBackupDataJob("@daily", mockGcRepo, mockSharedPoolRepo, mockObjectStoreRepo, mockBucketSvc)

	retention := uint32(10)
	gcSetting := newBackupGcSetting("sp-not-expired", time.Now().Add(-24*time.Hour), retention)

	mockGcRepo.EXPECT().List(mock.Anything, mock.AnythingOfType("*repo.BackupGcSettingFilter")).Return([]*entity.BackupGcSetting{gcSetting}, nil)

	err := job.Execute(ctx)
	require.NoError(t, err)
}

func TestCleanExpiredAutoBackupDataJob_Execute_LoadErrors(t *testing.T) {
	ctx := context.Background()

	retention := uint32(7)
	expiredAt := time.Now().Add(-time.Duration(retention+2) * 24 * time.Hour)

	t.Run("bucketInfoLoadError", func(t *testing.T) {
		mockGcRepo := repomocks.NewBackupGcSettingRepo(t)
		mockSharedPoolRepo := repomocks.NewSharedPoolRepo(t)
		mockObjectStoreRepo := repomocks.NewSharedPoolObjectStoreRepo(t)
		mockBucketSvc := dpmgrmocks.NewClient(t)

		job := NewCleanExpiredAutoBackupDataJob("@daily", mockGcRepo, mockSharedPoolRepo, mockObjectStoreRepo, mockBucketSvc)

		gcSetting := newBackupGcSetting("sp-error", expiredAt, retention)

		mockGcRepo.EXPECT().List(mock.Anything, mock.AnythingOfType("*repo.BackupGcSettingFilter")).Return([]*entity.BackupGcSetting{gcSetting}, nil)
		mockObjectStoreRepo.EXPECT().Load(mock.Anything, "sp-error", schema.SharedPoolObjectStoreTypeData, true).Return(nil, errors.New("load bucket error"))

		err := job.Execute(ctx)
		require.NoError(t, err)
	})

	t.Run("sharedPoolLoadError", func(t *testing.T) {
		mockGcRepo := repomocks.NewBackupGcSettingRepo(t)
		mockSharedPoolRepo := repomocks.NewSharedPoolRepo(t)
		mockObjectStoreRepo := repomocks.NewSharedPoolObjectStoreRepo(t)
		mockBucketSvc := dpmgrmocks.NewClient(t)

		job := NewCleanExpiredAutoBackupDataJob("@daily", mockGcRepo, mockSharedPoolRepo, mockObjectStoreRepo, mockBucketSvc)

		gcSetting := newBackupGcSetting("sp-error", expiredAt, retention)

		mockGcRepo.EXPECT().List(mock.Anything, mock.AnythingOfType("*repo.BackupGcSettingFilter")).Return([]*entity.BackupGcSetting{gcSetting}, nil)
		mockObjectStoreRepo.EXPECT().Load(mock.Anything, "sp-error", schema.SharedPoolObjectStoreTypeData, true).
			Return(&entity.SharedPoolObjectStore{SharedPoolObjectStore: schema.SharedPoolObjectStore{StoreBucket: "bucket"}}, nil)
		mockSharedPoolRepo.EXPECT().Load(mock.Anything, "sp-error", true).Return(nil, errors.New("shared pool load error"))

		err := job.Execute(ctx)
		require.NoError(t, err)
	})
}

func TestCleanExpiredAutoBackupDataJob_Execute_Success(t *testing.T) {
	ctx := context.Background()

	retention := uint32(7)
	expiredAt := time.Now().Add(-time.Duration(retention+5) * 24 * time.Hour)

	gcSetting := newBackupGcSetting("sp-success", expiredAt, retention)

	mockGcRepo := repomocks.NewBackupGcSettingRepo(t)
	mockSharedPoolRepo := repomocks.NewSharedPoolRepo(t)
	mockObjectStoreRepo := repomocks.NewSharedPoolObjectStoreRepo(t)
	mockBucketSvc := dpmgrmocks.NewClient(t)

	job := NewCleanExpiredAutoBackupDataJob("@daily", mockGcRepo, mockSharedPoolRepo, mockObjectStoreRepo, mockBucketSvc)

	mockGcRepo.EXPECT().List(mock.Anything, mock.AnythingOfType("*repo.BackupGcSettingFilter")).Return([]*entity.BackupGcSetting{gcSetting}, nil)

	mockObjectStoreRepo.EXPECT().Load(mock.Anything, "sp-success", schema.SharedPoolObjectStoreTypeData, true).
		Return(&entity.SharedPoolObjectStore{SharedPoolObjectStore: schema.SharedPoolObjectStore{StoreBucket: "bucket-1"}}, nil)

	sharedPool := &entity.SharedPool{
		SharedPool: apiv1.SharedPool{
			DataplaneName: "dp-1",
			ObjectStorage: &apiv1.SharedPool_ObjectStorage{
				Bucket: "bucket-1",
				Prefix: "/data",
			},
		},
	}

	mockSharedPoolRepo.EXPECT().Load(mock.Anything, "sp-success", true).Return(sharedPool, nil)

	mockBucketSvc.EXPECT().GetBucket(mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.GetBucketRequest) bool {
		return req.DataplaneId == "dp-1" && req.BucketId == "bucket-1"
	})).Return(&dplanemgrv1.Bucket{
		Name:   "bucket-1",
		Region: "us-west-2",
	}, nil)

	mockBucketSvc.EXPECT().ApplyLifecycleRules(mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.ApplyLifecycleRulesRequest) bool {
		require.Equal(t, "dp-1", req.DataplaneId)
		require.Equal(t, "bucket-1", req.BucketId)
		require.NotEmpty(t, req.Rules)
		return req.DataplaneId == "dp-1"
	})).Return(&emptypb.Empty{}, nil)

	mockGcRepo.EXPECT().Save(mock.Anything, mock.MatchedBy(func(setting *entity.BackupGcSetting) bool {
		return setting.Status == schema.BackupGcStatusAutoBackupExpired
	})).Return(nil)

	err := job.Execute(ctx)
	require.NoError(t, err)
	require.Equal(t, schema.BackupGcStatusAutoBackupExpired, gcSetting.Status)
}

func newBackupGcSetting(sharedPoolID string, deletedAt time.Time, retention uint32) *entity.BackupGcSetting {
	return &entity.BackupGcSetting{
		BackupGcSetting: schema.BackupGcSetting{
			SharedPoolID:          sharedPoolID,
			Status:                schema.BackupGcStatusSPDeleted,
			SharedPoolDeletedAt:   lo.ToPtr(deletedAt),
			MaxT1MetaLifetimeDays: lo.ToPtr(retention),
		},
	}
}
