package br

import (
	"context"
	"testing"

	sqlmock "github.com/DATA-DOG/go-sqlmock"
	"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/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/utilities/bizerr"
)

func TestBackupSettingRepoGetActiveForUpdateLocksCreatedSetting(t *testing.T) {
	db, mock := newBackupRepoMockDB(t)
	repo := &backupSettingRepoImpl{provider: backupRepoTestProvider{db: db}}

	mock.ExpectQuery("SELECT \\* FROM `backup_settings` WHERE \\(cluster_id = \\? AND state = \\?\\) AND `backup_settings`.`deleted_at` IS NULL ORDER BY `backup_settings`.`id` LIMIT \\? FOR UPDATE").
		WithArgs(uint64(101), schema.BackupSettingStateCreated, 1).
		WillReturnRows(sqlmock.NewRows([]string{"id", "cluster_id", "state"}).
			AddRow(uint64(1), uint64(101), schema.BackupSettingStateCreated))

	setting, err := repo.GetActiveForUpdate(t.Context(), 101)
	require.NoError(t, err)
	require.Equal(t, uint64(101), setting.ClusterId)
	require.Equal(t, schema.BackupSettingStateCreated, setting.State)
	require.NoError(t, mock.ExpectationsWereMet())
}

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

	ctx := t.Context()

	t.Run("success - transition from deleted to expired state (has manual backups)", func(t *testing.T) {
		mockBackupRepo := mockbrrepo.NewMockBackupRepo(ctrl)

		repo := &backupSettingRepoImpl{
			backupRepo: mockBackupRepo,
		}

		backupSetting := &schema.BackupSetting{
			ID:        1,
			ClusterId: 123,
			State:     schema.BackupSettingStateDeleted,
		}

		// Mock count auto backups returns 0
		mockBackupRepo.EXPECT().UnscopedCount(ctx, gomock.Any()).DoAndReturn(
			func(ctx context.Context, filter *br.QueryBackupsFilter) (int64, bizerr.BizError) {
				assert.Equal(t, []uint64{123}, filter.ClusterIds)
				assert.Equal(t, []schema.BackupTriggerType{schema.BackupTriggerTypeAuto}, filter.TriggerTypes)
				return int64(0), nil
			},
		)

		// Mock count manual backups returns 1
		mockBackupRepo.EXPECT().UnscopedCount(ctx, gomock.Any()).DoAndReturn(
			func(ctx context.Context, filter *br.QueryBackupsFilter) (int64, bizerr.BizError) {
				assert.Equal(t, []uint64{123}, filter.ClusterIds)
				assert.Equal(t, []schema.BackupTriggerType{schema.BackupTriggerTypeManual}, filter.TriggerTypes)
				return int64(1), nil
			},
		)

		changed, err := repo.changeBackupSettingStateIfNeeed(ctx, backupSetting)
		require.NoError(t, err)
		assert.True(t, changed)
		assert.Equal(t, schema.BackupSettingStateExpired, backupSetting.State)
	})

	t.Run("success - transition from deleted to cleanup state (no backups)", func(t *testing.T) {
		mockBackupRepo := mockbrrepo.NewMockBackupRepo(ctrl)

		repo := &backupSettingRepoImpl{
			backupRepo: mockBackupRepo,
		}

		backupSetting := &schema.BackupSetting{
			ID:        1,
			ClusterId: 123,
			State:     schema.BackupSettingStateDeleted,
		}

		// Mock count auto backups returns 0
		mockBackupRepo.EXPECT().UnscopedCount(ctx, gomock.Any()).DoAndReturn(
			func(ctx context.Context, filter *br.QueryBackupsFilter) (int64, bizerr.BizError) {
				assert.Equal(t, []uint64{123}, filter.ClusterIds)
				assert.Equal(t, []schema.BackupTriggerType{schema.BackupTriggerTypeAuto}, filter.TriggerTypes)
				return int64(0), nil
			},
		)

		// Mock count manual backups returns 0
		mockBackupRepo.EXPECT().UnscopedCount(ctx, gomock.Any()).DoAndReturn(
			func(ctx context.Context, filter *br.QueryBackupsFilter) (int64, bizerr.BizError) {
				assert.Equal(t, []uint64{123}, filter.ClusterIds)
				assert.Equal(t, []schema.BackupTriggerType{schema.BackupTriggerTypeManual}, filter.TriggerTypes)
				return int64(0), nil
			},
		)

		changed, err := repo.changeBackupSettingStateIfNeeed(ctx, backupSetting)
		require.NoError(t, err)
		assert.True(t, changed)
		assert.Equal(t, schema.BackupSettingStateCleanup, backupSetting.State)
	})

	t.Run("success - no transition needed - has auto backups", func(t *testing.T) {
		mockBackupRepo := mockbrrepo.NewMockBackupRepo(ctrl)

		repo := &backupSettingRepoImpl{
			backupRepo: mockBackupRepo,
		}

		backupSetting := &schema.BackupSetting{
			ID:        1,
			ClusterId: 123,
			State:     schema.BackupSettingStateDeleted,
		}

		// Mock count auto backups returns 1
		mockBackupRepo.EXPECT().UnscopedCount(ctx, gomock.Any()).DoAndReturn(
			func(ctx context.Context, filter *br.QueryBackupsFilter) (int64, bizerr.BizError) {
				assert.Equal(t, []uint64{123}, filter.ClusterIds)
				assert.Equal(t, []schema.BackupTriggerType{schema.BackupTriggerTypeAuto}, filter.TriggerTypes)
				return int64(1), nil
			},
		)

		changed, err := repo.changeBackupSettingStateIfNeeed(ctx, backupSetting)
		require.NoError(t, err)
		assert.False(t, changed)
		assert.Equal(t, schema.BackupSettingStateDeleted, backupSetting.State) // State unchanged
	})

	t.Run("success - no transition needed - already in expired state with manual backups", func(t *testing.T) {
		mockBackupRepo := mockbrrepo.NewMockBackupRepo(ctrl)

		repo := &backupSettingRepoImpl{
			backupRepo: mockBackupRepo,
		}

		backupSetting := &schema.BackupSetting{
			ID:        1,
			ClusterId: 123,
			State:     schema.BackupSettingStateExpired,
		}

		// Mock count manual backups returns 1
		mockBackupRepo.EXPECT().UnscopedCount(ctx, gomock.Any()).DoAndReturn(
			func(ctx context.Context, filter *br.QueryBackupsFilter) (int64, bizerr.BizError) {
				assert.Equal(t, []uint64{123}, filter.ClusterIds)
				assert.Equal(t, []schema.BackupTriggerType{schema.BackupTriggerTypeManual}, filter.TriggerTypes)
				return int64(1), nil
			},
		)

		changed, err := repo.changeBackupSettingStateIfNeeed(ctx, backupSetting)
		require.NoError(t, err)
		assert.False(t, changed)
		assert.Equal(t, schema.BackupSettingStateExpired, backupSetting.State) // State unchanged
	})

	t.Run("success - no transition needed - already in cleanup state", func(t *testing.T) {
		mockBackupRepo := mockbrrepo.NewMockBackupRepo(ctrl)

		repo := &backupSettingRepoImpl{
			backupRepo: mockBackupRepo,
		}

		backupSetting := &schema.BackupSetting{
			ID:        1,
			ClusterId: 123,
			State:     schema.BackupSettingStateCleanup,
		}

		// No mock calls expected since the method should return early

		changed, err := repo.changeBackupSettingStateIfNeeed(ctx, backupSetting)
		require.NoError(t, err)
		assert.False(t, changed)
		assert.Equal(t, schema.BackupSettingStateCleanup, backupSetting.State) // State unchanged
	})

	t.Run("success - no transition needed - not in deleted still have backups state", func(t *testing.T) {
		mockBackupRepo := mockbrrepo.NewMockBackupRepo(ctrl)

		repo := &backupSettingRepoImpl{
			backupRepo: mockBackupRepo,
		}

		backupSetting := &schema.BackupSetting{
			ID:        1,
			ClusterId: 123,
			State:     schema.BackupSettingStateCreated, // Not in deleted/expired state
		}

		// No mock calls expected since the method should return early

		changed, err := repo.changeBackupSettingStateIfNeeed(ctx, backupSetting)
		require.NoError(t, err)
		assert.False(t, changed)
		assert.Equal(t, schema.BackupSettingStateCreated, backupSetting.State) // State unchanged
	})

	t.Run("success - transition from expired to cleanup state", func(t *testing.T) {
		mockBackupRepo := mockbrrepo.NewMockBackupRepo(ctrl)

		repo := &backupSettingRepoImpl{
			backupRepo: mockBackupRepo,
		}

		backupSetting := &schema.BackupSetting{
			ID:        1,
			ClusterId: 123,
			State:     schema.BackupSettingStateExpired,
		}

		// Mock count manual backups returns 0
		mockBackupRepo.EXPECT().UnscopedCount(ctx, gomock.Any()).DoAndReturn(
			func(ctx context.Context, filter *br.QueryBackupsFilter) (int64, bizerr.BizError) {
				assert.Equal(t, []uint64{123}, filter.ClusterIds)
				assert.Equal(t, []schema.BackupTriggerType{schema.BackupTriggerTypeManual}, filter.TriggerTypes)
				return int64(0), nil
			},
		)

		changed, err := repo.changeBackupSettingStateIfNeeed(ctx, backupSetting)
		require.NoError(t, err)
		assert.True(t, changed)
		assert.Equal(t, schema.BackupSettingStateCleanup, backupSetting.State)
	})

	t.Run("error - deleted state - count auto backups fails", func(t *testing.T) {
		mockBackupRepo := mockbrrepo.NewMockBackupRepo(ctrl)

		repo := &backupSettingRepoImpl{
			backupRepo: mockBackupRepo,
		}

		backupSetting := &schema.BackupSetting{
			ID:        1,
			ClusterId: 123,
			State:     schema.BackupSettingStateDeleted,
		}

		// Mock count auto backups fails
		expectedErr := bizerr.New(bizerr.ErrCodeDB, "count error")
		mockBackupRepo.EXPECT().UnscopedCount(ctx, gomock.Any()).DoAndReturn(
			func(ctx context.Context, filter *br.QueryBackupsFilter) (int64, bizerr.BizError) {
				assert.Equal(t, []uint64{123}, filter.ClusterIds)
				assert.Equal(t, []schema.BackupTriggerType{schema.BackupTriggerTypeAuto}, filter.TriggerTypes)
				return int64(0), expectedErr
			},
		)

		changed, err := repo.changeBackupSettingStateIfNeeed(ctx, backupSetting)
		require.Error(t, err)
		assert.False(t, changed)
		assert.Contains(t, err.Error(), "failed to count auto backups")
		assert.Equal(t, schema.BackupSettingStateDeleted, backupSetting.State) // State unchanged
	})

	t.Run("error - deleted state - count manual backups fails", func(t *testing.T) {
		mockBackupRepo := mockbrrepo.NewMockBackupRepo(ctrl)

		repo := &backupSettingRepoImpl{
			backupRepo: mockBackupRepo,
		}

		backupSetting := &schema.BackupSetting{
			ID:        1,
			ClusterId: 123,
			State:     schema.BackupSettingStateDeleted,
		}

		// Mock count auto backups returns 0
		mockBackupRepo.EXPECT().UnscopedCount(ctx, gomock.Any()).DoAndReturn(
			func(ctx context.Context, filter *br.QueryBackupsFilter) (int64, bizerr.BizError) {
				assert.Equal(t, []uint64{123}, filter.ClusterIds)
				assert.Equal(t, []schema.BackupTriggerType{schema.BackupTriggerTypeAuto}, filter.TriggerTypes)
				return int64(0), nil
			},
		)

		// Mock count manual backups fails
		expectedErr := bizerr.New(bizerr.ErrCodeDB, "count error")
		mockBackupRepo.EXPECT().UnscopedCount(ctx, gomock.Any()).DoAndReturn(
			func(ctx context.Context, filter *br.QueryBackupsFilter) (int64, bizerr.BizError) {
				assert.Equal(t, []uint64{123}, filter.ClusterIds)
				assert.Equal(t, []schema.BackupTriggerType{schema.BackupTriggerTypeManual}, filter.TriggerTypes)
				return int64(0), expectedErr
			},
		)

		changed, err := repo.changeBackupSettingStateIfNeeed(ctx, backupSetting)
		require.Error(t, err)
		assert.False(t, changed)
		assert.Contains(t, err.Error(), "failed to count manual backups")
		assert.Equal(t, schema.BackupSettingStateDeleted, backupSetting.State) // State unchanged
	})

	t.Run("error - expired state - count manual backups fails", func(t *testing.T) {
		mockBackupRepo := mockbrrepo.NewMockBackupRepo(ctrl)

		repo := &backupSettingRepoImpl{
			backupRepo: mockBackupRepo,
		}

		backupSetting := &schema.BackupSetting{
			ID:        1,
			ClusterId: 123,
			State:     schema.BackupSettingStateExpired,
		}

		// Mock count manual backups fails
		expectedErr := bizerr.New(bizerr.ErrCodeDB, "count error")
		mockBackupRepo.EXPECT().UnscopedCount(ctx, gomock.Any()).DoAndReturn(
			func(ctx context.Context, filter *br.QueryBackupsFilter) (int64, bizerr.BizError) {
				assert.Equal(t, []uint64{123}, filter.ClusterIds)
				assert.Equal(t, []schema.BackupTriggerType{schema.BackupTriggerTypeManual}, filter.TriggerTypes)
				return int64(0), expectedErr
			},
		)

		changed, err := repo.changeBackupSettingStateIfNeeed(ctx, backupSetting)
		require.Error(t, err)
		assert.False(t, changed)
		assert.Contains(t, err.Error(), "failed to count manual backups")
		assert.Equal(t, schema.BackupSettingStateExpired, backupSetting.State) // State unchanged
	})
}
