package backup_test

import (
	"context"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/dataflow-service/app/repo"
	"github.com/tidbcloud/dataflow-service/app/repo/backup"
	"github.com/tidbcloud/dataflow-service/app/repo/entity"
	"github.com/tidbcloud/pkgv2/db"
	"gorm.io/gorm"
)

func getBackupScheduleRepoAndWdb(t *testing.T) (backup.BackupScheduleRepo, *gorm.DB) {
	testDB, err := repo.GetFakeDB()
	require.NoError(t, err)
	dbProvider := db.NewProvider(testDB)
	return backup.NewBackupScheduleRepo(dbProvider), testDB
}

func Test_BackupScheduleRepo_SoftDelete_InvalidInput(t *testing.T) {
	repo, _ := getBackupScheduleRepoAndWdb(t)
	err := repo.SoftDelete(context.Background(), 0)
	require.True(t, strings.Contains(err.Error(), "id is required"))

	// silent for not found
	err = repo.SoftDelete(context.Background(), 7767)
	require.Nil(t, err)
}

func Test_BackupScheduleRepo_SoftDelete_Idempotent(t *testing.T) {
	repo, wdb := getBackupScheduleRepoAndWdb(t)
	deletedAt := time.Date(2023, time.January, 2, 15, 4, 5, 0, time.UTC)
	bs := &entity.BackupSchedule{
		Type:        "auto",
		BackupCycle: entity.BackupCycleDaily,
		Method:      "full",
		ClusterType: "dedicated",
		DeletedAt:   &deletedAt,
	}
	bs.ID = entity.ID(7768)
	require.Nil(t, repo.Create(context.Background(), bs))

	abs := entity.BackupSchedule{}
	err := wdb.Where("id = ?", bs.ID).First(&abs).Error
	require.Nil(t, err)
	require.NotNil(t, abs.DeletedAt)

	repo.SoftDelete(context.Background(), bs.ID)

	abs2 := entity.BackupSchedule{}
	err = wdb.Where("id = ?", bs.ID).First(&abs2).Error
	require.Nil(t, err)
	require.Equal(t, deletedAt, *abs2.DeletedAt, "should not change previous DeletedAt")
}

func Test_BackupScheduleRepo_SoftDelete_Happy(t *testing.T) {
	repo, wdb := getBackupScheduleRepoAndWdb(t)
	clusterID1 := entity.ID(7769)
	bs1 := &entity.BackupSchedule{
		ClusterID:   clusterID1,
		Type:        "auto",
		BackupCycle: entity.BackupCycleDaily,
		Method:      "full",
		ClusterType: "dedicated",
	}
	bs1.ID = entity.ID(77691)
	bs2 := &entity.BackupSchedule{
		ClusterID:   clusterID1,
		Type:        "manual",
		BackupCycle: entity.BackupCycleDaily,
		Method:      "full",
		ClusterType: "dedicated",
	}
	bs2.ID = entity.ID(77692)

	clusterID2 := entity.ID(7770)
	bs3 := &entity.BackupSchedule{
		ClusterID:   clusterID2,
		Type:        "auto",
		BackupCycle: entity.BackupCycleDaily,
		Method:      "full",
		ClusterType: "dedicated",
	}
	bs3.ID = entity.ID(7770)

	require.Nil(t, repo.Create(context.Background(), bs1))
	require.Nil(t, repo.Create(context.Background(), bs2))
	require.Nil(t, repo.Create(context.Background(), bs3))

	abs11 := entity.BackupSchedule{}
	err := wdb.Where("id = ?", bs1.ID).First(&abs11).Error
	require.Nil(t, err)

	time.Sleep(10 * time.Millisecond)
	require.Nil(t, repo.SoftDelete(context.Background(), bs1.ID))

	abs12 := entity.BackupSchedule{}
	err = wdb.Where("id = ?", bs1.ID).First(&abs12).Error
	require.Nil(t, err)
	require.NotNil(t, abs12.DeletedAt)
	require.Equal(t, abs11.Versioning+1, abs12.Versioning)
	require.NotEqual(t, *abs11.UpdatedAt, *abs12.UpdatedAt)

	// not side effect to others schedule
	abs2 := entity.BackupSchedule{}
	err = wdb.Where("id = ?", bs2.ID).First(&abs2).Error
	require.Nil(t, err)
	require.Nil(t, abs2.DeletedAt)

	abs3 := entity.BackupSchedule{}
	err = wdb.Where("id = ?", bs3.ID).First(&abs3).Error
	require.Nil(t, err)
	require.Nil(t, abs3.DeletedAt)
}

func Test_BackupScheduleRepo_SoftDeleteAuto_Happy(t *testing.T) {
	repo, wdb := getBackupScheduleRepoAndWdb(t)
	clusterID := entity.ID(7771)
	bs1 := &entity.BackupSchedule{
		ClusterID:   clusterID,
		Type:        "auto",
		BackupCycle: entity.BackupCycleDaily,
		Method:      "full",
		ClusterType: "dedicated",
	}
	bs2 := &entity.BackupSchedule{
		ClusterID:   clusterID,
		Type:        "manual",
		BackupCycle: entity.BackupCycleDaily,
		Method:      "full",
		ClusterType: "dedicated",
	}

	require.Nil(t, repo.Create(context.Background(), bs1))
	require.Nil(t, repo.Create(context.Background(), bs2))

	abs11 := entity.BackupSchedule{}
	err := wdb.Where("cluster_id = ? and type = 'auto'", clusterID).First(&abs11).Error
	require.Nil(t, err)

	time.Sleep(10 * time.Millisecond)
	require.Nil(t, repo.SoftDeleteAuto(context.Background(), clusterID))

	abs12 := entity.BackupSchedule{}
	err = wdb.Where("cluster_id = ? and type = 'auto'", clusterID).First(&abs12).Error
	require.Nil(t, err)
	require.NotNil(t, abs12.DeletedAt)
	require.Equal(t, abs11.Versioning+1, abs12.Versioning)
	require.NotEqual(t, *abs11.UpdatedAt, *abs12.UpdatedAt)

	// not side effect to manual schedule
	abs2 := entity.BackupSchedule{}
	err = wdb.Where("cluster_id = ? and type = 'manual'", clusterID).First(&abs2).Error
	require.Nil(t, err)
	require.Nil(t, abs2.DeletedAt)
}

func Test_BackupScheduleRepo_SoftDeleteAuto_Idempotent(t *testing.T) {
	repo, wdb := getBackupScheduleRepoAndWdb(t)
	deletedAt := time.Date(2023, time.January, 2, 15, 4, 5, 0, time.UTC)
	clusterID := entity.ID(7773)
	bs := &entity.BackupSchedule{
		ClusterID:   clusterID,
		Type:        "auto",
		BackupCycle: entity.BackupCycleDaily,
		Method:      "full",
		ClusterType: "dedicated",
		DeletedAt:   &deletedAt,
	}
	require.Nil(t, repo.Create(context.Background(), bs))

	abs := entity.BackupSchedule{}
	err := wdb.Where("cluster_Id = ?", clusterID).First(&abs).Error
	require.Nil(t, err)
	require.NotNil(t, abs.DeletedAt)

	repo.SoftDeleteAuto(context.Background(), clusterID)

	abs2 := entity.BackupSchedule{}
	err = wdb.Where("cluster_Id = ?", clusterID).First(&abs2).Error
	require.Nil(t, err)
	require.Equal(t, deletedAt, *abs2.DeletedAt, "should not change previous DeletedAt")
}

func Test_BackupScheduleRepo_SoftDeleteAuto_InvalidInput(t *testing.T) {
	repo, _ := getBackupScheduleRepoAndWdb(t)
	err := repo.SoftDeleteAuto(context.Background(), 0)
	require.True(t, strings.Contains(err.Error(), "cluster id is required"))
}

func Test_BackupScheduleRepo_PatchAuto_Happy(t *testing.T) {
	repo, wdb := getBackupScheduleRepoAndWdb(t)
	clusterID := entity.ID(7774)
	bs1 := &entity.BackupSchedule{
		ClusterID:       clusterID,
		Type:            "auto",
		BackupCycle:     entity.BackupCycleDaily,
		Method:          "full",
		ClusterType:     "dedicated",
		RetentionPeriod: 7,
	}
	bs2 := &entity.BackupSchedule{
		ClusterID:       clusterID,
		Type:            "manual",
		BackupCycle:     entity.BackupCycleDaily,
		Method:          "full",
		ClusterType:     "dedicated",
		RetentionPeriod: 7,
	}

	require.Nil(t, repo.Create(context.Background(), bs1))
	require.Nil(t, repo.Create(context.Background(), bs2))

	bs1BeforePatchAuto := entity.BackupSchedule{}
	err := wdb.Where("cluster_Id = ? and type = 'auto'", clusterID).First(&bs1BeforePatchAuto).Error
	require.Nil(t, err)
	bs2BeforePatchAuto := entity.BackupSchedule{}
	err = wdb.Where("cluster_Id = ? and type = 'manual'", clusterID).First(&bs2BeforePatchAuto).Error
	require.Nil(t, err)

	// make updated_at different
	time.Sleep(1 * time.Second)

	err = repo.PatchAuto(context.Background(), clusterID, map[string]interface{}{
		"schedule":         "dummy",
		"retention_period": 0,
		"next_backup_time": time.Now(),
	})
	require.Nil(t, err)

	bs1AfterPatchAuto := entity.BackupSchedule{}
	err = wdb.Where("cluster_Id = ? and type = 'auto'", clusterID).First(&bs1AfterPatchAuto).Error
	require.Nil(t, err)

	require.NotEqual(t, *bs1BeforePatchAuto.UpdatedAt, *bs1AfterPatchAuto.UpdatedAt,
		"UpdatedAt should be updated automatically")
	require.Equal(t, bs1BeforePatchAuto.Versioning+1, bs1AfterPatchAuto.Versioning, "Versioning should be increased")
	require.Equal(t, "dummy", bs1AfterPatchAuto.Schedule, "Value should be patched successfully")
	require.Equal(t, uint64(0), bs1AfterPatchAuto.RetentionPeriod, "Zero value should be patched successfully")
	require.Equal(t, clusterID, bs1AfterPatchAuto.ClusterID, "Undefined field should not be overridden")
	require.NotNil(t, bs1AfterPatchAuto.NextBackupTime, "NextBackupTime should not nil")

	// verify there is no change to manual BackupSchedule, i.e. bs2
	bs2AfterPatchAuto := entity.BackupSchedule{}
	err = wdb.Where("cluster_Id = ? and type = 'manual'", clusterID).First(&bs2AfterPatchAuto).Error
	require.Nil(t, err)
	require.Equal(t, *bs2BeforePatchAuto.UpdatedAt, *bs2AfterPatchAuto.UpdatedAt)
	require.Equal(t, bs2BeforePatchAuto.Versioning, bs2AfterPatchAuto.Versioning)
	require.NotEqual(t, "dummy", bs2AfterPatchAuto.Schedule)
	require.Equal(t, bs2BeforePatchAuto.RetentionPeriod, bs2AfterPatchAuto.RetentionPeriod)
}

func Test_BackupScheduleRepo_PatchAuto_InvalidInput(t *testing.T) {
	repo, _ := getBackupScheduleRepoAndWdb(t)

	err := repo.PatchAuto(context.Background(), 0, nil)
	require.True(t, strings.Contains(err.Error(), "cluster id is required"))

	err = repo.PatchAuto(context.Background(), 1, map[string]interface{}{})
	require.True(t, strings.Contains(err.Error(), "no content in patches"))

	err = repo.PatchAuto(context.Background(), 1, nil)
	require.True(t, strings.Contains(err.Error(), "no content in patches"))

	clusterID := entity.ID(7775)
	bs := &entity.BackupSchedule{
		ClusterID:       clusterID,
		Type:            "manual",
		BackupCycle:     entity.BackupCycleDaily,
		Method:          "full",
		ClusterType:     "dedicated",
		RetentionPeriod: 7,
	}
	require.Nil(t, repo.Create(context.Background(), bs))

	err = repo.PatchAuto(context.Background(), 1, map[string]interface{}{"a": "b"})
	require.True(t, strings.Contains(err.Error(), "record not found"))
	err = repo.PatchAuto(context.Background(), clusterID, map[string]interface{}{"a": "b"})
	require.True(t, strings.Contains(err.Error(), "record not found"))

	clusterID = entity.ID(7776)
	bs = &entity.BackupSchedule{
		ClusterID:       clusterID,
		Type:            "auto",
		BackupCycle:     entity.BackupCycleDaily,
		Method:          "full",
		ClusterType:     "dedicated",
		RetentionPeriod: 7,
	}
	require.Nil(t, repo.Create(context.Background(), bs))
	err = repo.PatchAuto(context.Background(), clusterID, map[string]interface{}{"a": "b"})
	require.True(t, strings.Contains(err.Error(), "column \"a\" could not be found"))
}

func Test_BackupScheduleRepo_Patch_Happy(t *testing.T) {
	repo, wdb := getBackupScheduleRepoAndWdb(t)
	bs1 := &entity.BackupSchedule{
		Type:            "auto",
		BackupCycle:     entity.BackupCycleDaily,
		Method:          "full",
		ClusterType:     "dedicated",
		RetentionPeriod: 7,
	}
	bs1.ID = entity.ID(7777)
	bs2 := &entity.BackupSchedule{
		Type:            "manual",
		BackupCycle:     entity.BackupCycleDaily,
		Method:          "full",
		ClusterType:     "dedicated",
		RetentionPeriod: 7,
	}
	bs2.ID = entity.ID(7778)

	require.Nil(t, repo.Create(context.Background(), bs1))
	require.Nil(t, repo.Create(context.Background(), bs2))

	bs1BeforePatch := entity.BackupSchedule{}
	err := wdb.Where("id = ?", bs1.ID).First(&bs1BeforePatch).Error
	require.Nil(t, err)
	bs2BeforePatch := entity.BackupSchedule{}
	err = wdb.Where("id = ?", bs2.ID).First(&bs2BeforePatch).Error
	require.Nil(t, err)

	// make updated_at different
	time.Sleep(1 * time.Second)

	err = repo.Patch(context.Background(), bs1.ID, map[string]interface{}{
		"schedule":         "dummy",
		"retention_period": 0,
	})
	require.Nil(t, err)

	bs1AfterPatch := entity.BackupSchedule{}
	err = wdb.Where("id = ?", bs1.ID).First(&bs1AfterPatch).Error
	require.Nil(t, err)

	require.NotEqual(t, *bs1BeforePatch.UpdatedAt, *bs1AfterPatch.UpdatedAt,
		"UpdatedAt should be updated automatically")
	require.Equal(t, bs1BeforePatch.Versioning+1, bs1AfterPatch.Versioning, "Versioning should be increased")
	require.Equal(t, "dummy", bs1AfterPatch.Schedule, "Value should be patched successfully")
	require.Equal(t, uint64(0), bs1AfterPatch.RetentionPeriod, "Zero value should be patched successfully")

	// verify there is no change to other BackupSchedule, i.e. bs2
	bs2AfterPatch := entity.BackupSchedule{}
	err = wdb.Where("id = ?", bs2.ID).First(&bs2AfterPatch).Error
	require.Nil(t, err)
	require.Equal(t, *bs2BeforePatch.UpdatedAt, *bs2AfterPatch.UpdatedAt)
	require.Equal(t, bs2BeforePatch.Versioning, bs2AfterPatch.Versioning)
	require.NotEqual(t, "dummy", bs2AfterPatch.Schedule)
	require.Equal(t, bs2BeforePatch.RetentionPeriod, bs2AfterPatch.RetentionPeriod)
}

func Test_BackupScheduleRepo_Patch_InvalidInput(t *testing.T) {
	repo, _ := getBackupScheduleRepoAndWdb(t)

	err := repo.Patch(context.Background(), 0, nil)
	require.True(t, strings.Contains(err.Error(), "id is required"))

	err = repo.Patch(context.Background(), 1, map[string]interface{}{})
	require.True(t, strings.Contains(err.Error(), "no content in patches"))

	err = repo.Patch(context.Background(), 1, nil)
	require.True(t, strings.Contains(err.Error(), "no content in patches"))

	err = repo.Patch(context.Background(), 1, map[string]interface{}{"a": "b"})
	require.True(t, strings.Contains(err.Error(), "column \"a\" could not be found"))

	bs := &entity.BackupSchedule{
		Type:            "auto",
		BackupCycle:     entity.BackupCycleDaily,
		Method:          "full",
		ClusterType:     "dedicated",
		RetentionPeriod: 7,
	}
	bs.ID = entity.ID(7779)
	require.Nil(t, repo.Create(context.Background(), bs))
	err = repo.Patch(context.Background(), bs.ID, map[string]interface{}{"a": "b"})
	require.True(t, strings.Contains(err.Error(), "column \"a\" could not be found"))
}
