package entity_test

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"testing"
	"time"

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

func getDBWriter(t *testing.T) *gorm.DB {
	testDB, err := repo.GetFakeDB()
	require.NoError(t, err)
	dbProvider := db.NewProvider(testDB)
	return dbProvider.WriteDB(context.Background())
}

func Test_SaveWithVersioning(t *testing.T) {
	wdb := getDBWriter(t)

	// create a record
	recordName := "SaveWithVersioning_Record"
	bs := &entity.BackupSchedule{
		Name:        recordName,
		Type:        "auto",
		BackupCycle: entity.BackupCycleDaily,
		Method:      "full",
		ClusterType: "dedicated",
	}
	tx := wdb.Save(bs)
	require.NoError(t, tx.Error)

	// get the record
	firstVersion := &entity.BackupSchedule{}
	entity.NewBackupScheduleQuerySet(wdb).NameEq(recordName).One(firstVersion)

	// clone and update
	firstUpdate := *firstVersion
	firstUpdate.BackupCycle = entity.BackupCycleWeekly
	err := entity.SaveWithVersioning(wdb, &firstUpdate)
	require.NoError(t, err, "there should be no error since first version is the latest one")

	// clone and update again
	secondUpdate := *firstVersion
	secondUpdate.BackupCycle = entity.BackupCycleDaily
	err = entity.SaveWithVersioning(wdb, &secondUpdate)
	require.Error(t, err, "there should be error since first version is not the latest one")
	require.Equal(t, bizerr.ErrCodeOptimisticLockConflict, err.ErrCode())
}

func Test_RetrySaveWithVersioning_Happy(t *testing.T) {
	wdb := getDBWriter(t)

	// create a record
	recordName := "RetrySaveWithVersioning_Happy_Record"
	bs := &entity.BackupSchedule{
		Name:        recordName,
		Type:        "auto",
		BackupCycle: entity.BackupCycleDaily,
		Method:      "full",
		ClusterType: "dedicated",
	}
	tx := wdb.Save(bs)
	require.NoError(t, tx.Error)

	// get the record
	firstVersion := &entity.BackupSchedule{}
	entity.NewBackupScheduleQuerySet(wdb).NameEq(recordName).One(firstVersion)

	// clone and update
	firstUpdate := *firstVersion
	firstUpdate.BackupCycle = entity.BackupCycleWeekly
	err := entity.SaveWithVersioning(wdb, &firstUpdate)
	require.NoError(t, err, "there should be no error since first version is the latest one")

	// get latest and update
	retryerr := entity.DefaultRetrySaveWithVersioning(wdb,
		func(wdb *gorm.DB) (value interface{}, err error) {
			latest := &entity.BackupSchedule{}
			innerErr := entity.NewBackupScheduleQuerySet(wdb).NameEq(recordName).One(latest)
			return latest, innerErr
		}, func(latest interface{}) (updated interface{}, err error) {
			latestBS := latest.(*entity.BackupSchedule)
			latestBS.BackupCycle = entity.BackupCycleDaily
			return latestBS, nil
		})
	require.NoError(t, retryerr, "there should be no error since we fetch latest first then update")
}

func Test_RetrySaveWithVersioning_RetrySuccess(t *testing.T) {
	wdb := getDBWriter(t)

	// create a record
	recordName := "RetrySaveWithVersioning_RetrySuccess_Record"
	bs := &entity.BackupSchedule{
		Name:        recordName,
		Type:        "auto",
		BackupCycle: entity.BackupCycleDaily,
		Method:      "full",
		ClusterType: "dedicated",
	}
	tx := wdb.Save(bs)
	require.NoError(t, tx.Error)

	// get the record
	firstVersion := &entity.BackupSchedule{}
	entity.NewBackupScheduleQuerySet(wdb).NameEq(recordName).One(firstVersion)

	// clone and update
	firstUpdate := *firstVersion
	firstUpdate.BackupCycle = entity.BackupCycleWeekly
	err := entity.SaveWithVersioning(wdb, &firstUpdate)
	require.NoError(t, err, "there should be no error since first version is the latest one")

	// retry and success
	retry := 0
	retryerr := entity.DefaultRetrySaveWithVersioning(wdb,
		func(wdb *gorm.DB) (value interface{}, err error) {
			retry++
			// The simulation of query old value
			if retry == 1 {
				secondUpdate := *firstVersion
				return &secondUpdate, nil
			}
			latest := &entity.BackupSchedule{}
			innerErr := entity.NewBackupScheduleQuerySet(wdb).NameEq(recordName).One(latest)
			return latest, innerErr
		}, func(latest interface{}) (updated interface{}, err error) {
			latestBS := latest.(*entity.BackupSchedule)
			latestBS.BackupCycle = entity.BackupCycleDaily
			return latestBS, nil
		})
	require.NoError(t, retryerr, "there should be no error since we fetch latest first then update")
	require.Equal(t, 2, retry, "should be successful at the second time")
}

func Test_RetrySaveWithVersioning_QueryError(t *testing.T) {
	wdb := getDBWriter(t)

	retry := 0
	queryErr := fmt.Errorf("query error")
	updateErr := fmt.Errorf("update error")
	retryerr := entity.DefaultRetrySaveWithVersioning(wdb,
		func(wdb *gorm.DB) (value interface{}, err error) {
			retry++
			return nil, queryErr
		}, func(latest interface{}) (updated interface{}, err error) {
			return nil, updateErr
		})
	require.Error(t, retryerr, "there should be error since query always return error")
	require.True(t, errors.Is(retryerr, queryErr), "there should be query error")
	require.Equal(t, 3, retry, "should use up retry times")
}

func Test_RetrySaveWithVersioning_QueryNil(t *testing.T) {
	wdb := getDBWriter(t)

	queryTimes := 0
	updateTimes := 0
	retryerr := entity.DefaultRetrySaveWithVersioning(wdb,
		func(wdb *gorm.DB) (value interface{}, err error) {
			queryTimes++
			return nil, nil
		}, func(latest interface{}) (updated interface{}, err error) {
			updateTimes++
			return nil, nil
		})
	require.True(t, strings.Contains(retryerr.Error(), "query get nil record"), "there should be error since query nil")
	require.Equal(t, 3, queryTimes, "should use up retry times")
	require.Equal(t, 0, updateTimes, "no update action trigger")
}

func Test_RetrySaveWithVersioning_UpdateError(t *testing.T) {
	wdb := getDBWriter(t)

	retry := 0
	updateErr := fmt.Errorf("update error")
	retryerr := entity.DefaultRetrySaveWithVersioning(wdb,
		func(wdb *gorm.DB) (value interface{}, err error) {
			retry++
			return "dummy", nil
		}, func(latest interface{}) (updated interface{}, err error) {
			return nil, updateErr
		})
	require.Error(t, retryerr, "there should be error since update always return error")
	require.True(t, errors.Is(retryerr, updateErr), "there should be query error")
	require.Equal(t, 3, retry, "should use up retry times")
}

func Test_RetrySaveWithVersioning_UpdateNothing(t *testing.T) {
	wdb := getDBWriter(t)

	// create a record
	recordName := "RetrySaveWithVersioning_UpdateNothing_Record"
	t1 := time.Date(2023, time.January, 2, 15, 4, 5, 0, time.UTC)
	t2 := time.Date(2024, time.January, 2, 15, 4, 5, 0, time.UTC)

	bs := &entity.BackupSchedule{
		Name:        recordName,
		Type:        "auto",
		BackupCycle: entity.BackupCycleDaily,
		Method:      "full",
		ClusterType: "dedicated",
		DeletedAt:   &t1,
	}
	tx := wdb.Save(bs)
	require.NoError(t, tx.Error)

	retryerr := entity.DefaultRetrySaveWithVersioning(wdb,
		func(wdb *gorm.DB) (value interface{}, err error) {
			var backupSchedule entity.BackupSchedule
			err = wdb.Model(&entity.BackupSchedule{}).Where("name = ?", recordName).First(&backupSchedule).Error
			return backupSchedule, err
		}, func(latest interface{}) (updated interface{}, err error) {
			input := latest.(entity.BackupSchedule)
			if input.DeletedAt != nil {
				return nil, nil
			}
			input.DeletedAt = &t2
			return input, nil
		})

	require.NoError(t, retryerr)
	var actualBS entity.BackupSchedule
	wdb.Model(&entity.BackupSchedule{}).Where("name = ?", recordName).First(&actualBS)
	require.Equal(t, t1, *actualBS.DeletedAt, "DeletedAt should not be changed since update return nil")
	require.NotEqual(t, t2, *actualBS.DeletedAt, "DeletedAt should not be changed since update return nil")
}

func Test_RetrySaveWithVersioning_SaveError(t *testing.T) {
	wdb := getDBWriter(t)

	// create a record
	recordName := "RetrySaveWithVersioning_UpdateError_Record"
	bs := &entity.BackupSchedule{
		Name:        recordName,
		Type:        "auto",
		BackupCycle: entity.BackupCycleDaily,
		Method:      "full",
		ClusterType: "dedicated",
	}
	tx := wdb.Save(bs)
	require.NoError(t, tx.Error)

	// get the record
	firstVersion := &entity.BackupSchedule{}
	entity.NewBackupScheduleQuerySet(wdb).NameEq(recordName).One(firstVersion)

	// clone and update
	firstUpdate := *firstVersion
	firstUpdate.BackupCycle = entity.BackupCycleWeekly
	err := entity.SaveWithVersioning(wdb, &firstUpdate)
	require.NoError(t, err, "there should be no error since first version is the latest one")

	retry := 0
	retryerr := entity.DefaultRetrySaveWithVersioning(wdb,
		func(wdb *gorm.DB) (value interface{}, err error) {
			retry++
			staleVersion := *firstVersion
			return staleVersion, nil
		}, func(latest interface{}) (updated interface{}, err error) {
			staleVersion := latest.(entity.BackupSchedule)
			staleVersion.BackupCycle = entity.BackupCycleWeekly
			return &staleVersion, nil
		})
	require.Error(t, retryerr, "there should be save error since version is stale")
	require.True(t, strings.Contains(retryerr.Error(), "save error"), "should be save error")
	require.Equal(t, 3, retry, "should use up retry times")
}
