package common

import (
	"testing"
	"time"

	sqlmock "github.com/DATA-DOG/go-sqlmock"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"

	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/infra/repoimpl/schema"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

type versionedTestModel struct {
	schema.Versioned

	ID        uint64 `gorm:"primaryKey"`
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt gorm.DeletedAt `gorm:"index"`
	Name      string
	Ignored   string
}

func (m *versionedTestModel) TableName() string {
	return "versioned_test_models"
}

func (m *versionedTestModel) GetId() uint64 {
	return m.ID
}

func newMockDB(t *testing.T) (*gorm.DB, sqlmock.Sqlmock) {
	t.Helper()

	sqlDB, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	require.NoError(t, err)
	t.Cleanup(func() {
		_ = sqlDB.Close()
	})

	db, err := gorm.Open(mysql.New(mysql.Config{
		Conn:                      sqlDB,
		SkipInitializeWithVersion: true,
	}), &gorm.Config{
		SkipDefaultTransaction: true,
		Logger:                 logger.Default.LogMode(logger.Silent),
	})
	require.NoError(t, err)
	return db, mock
}

func TestSaveWithVersioning_CreateWhenRecordNotFound(t *testing.T) {
	db, mock := newMockDB(t)

	model := &versionedTestModel{
		Versioned: schema.Versioned{Versioning: 1},
		ID:        42,
		Name:      "cluster-a",
		Ignored:   "secret",
	}

	mock.ExpectQuery("SELECT .*versioning.*FROM .*versioned_test_models.*").
		WillReturnRows(sqlmock.NewRows([]string{"versioning"}))
	mock.ExpectExec("INSERT INTO .*versioned_test_models.*").
		WillReturnResult(sqlmock.NewResult(42, 1))

	err := SaveWithVersioning(db, model, nil)
	require.NoError(t, err)
	require.NoError(t, mock.ExpectationsWereMet())
}

func TestSaveWithVersioning_UpdateUsesSingleOptimisticLockGuardedUpdate(t *testing.T) {
	db, mock := newMockDB(t)

	model := &versionedTestModel{
		Versioned: schema.Versioned{Versioning: 2},
		ID:        42,
		Name:      "cluster-a",
		Ignored:   "secret",
	}

	mock.ExpectQuery("SELECT .*versioning.*FROM .*versioned_test_models.*").
		WillReturnRows(sqlmock.NewRows([]string{"versioning"}).AddRow(2))
	mock.ExpectExec("UPDATE .*versioned_test_models.*").
		WillReturnResult(sqlmock.NewResult(0, 1))

	err := SaveWithVersioning(db, model, []string{"ignored"})
	require.NoError(t, err)
	assert.Equal(t, 3, model.Versioning)
	require.NoError(t, mock.ExpectationsWereMet())
}

func TestSaveWithVersioning_ReturnsOptimisticLockConflictWhenUpdateAffectsNoRows(t *testing.T) {
	db, mock := newMockDB(t)

	model := &versionedTestModel{
		Versioned: schema.Versioned{Versioning: 2},
		ID:        42,
		Name:      "cluster-a",
	}

	mock.ExpectQuery("SELECT .*versioning.*FROM .*versioned_test_models.*").
		WillReturnRows(sqlmock.NewRows([]string{"versioning"}).AddRow(2))
	mock.ExpectExec("UPDATE .*versioned_test_models.*").
		WillReturnResult(sqlmock.NewResult(0, 0))

	err := SaveWithVersioning(db, model, nil)
	require.Error(t, err)
	assert.Equal(t, bizerr.ErrCodeOptimisticLockConflict, err.ErrCode())
	require.NoError(t, mock.ExpectationsWereMet())
}

func TestSaveWithVersioning_UpdatesSoftDeletedRecord(t *testing.T) {
	db, mock := newMockDB(t)

	deletedAt := gorm.DeletedAt{Time: time.Now().UTC(), Valid: true}
	model := &versionedTestModel{
		Versioned: schema.Versioned{Versioning: 7},
		ID:        42,
		DeletedAt: deletedAt,
		Name:      "cluster-a",
	}

	mock.ExpectQuery("^SELECT .*versioning.* FROM .*versioned_test_models.* WHERE .*id.*=\\s*\\? LIMIT \\?$").
		WillReturnRows(sqlmock.NewRows([]string{"versioning"}).AddRow(7))
	mock.ExpectExec("^UPDATE .*versioned_test_models.* SET .* WHERE \\(id=\\? AND versioning=\\?\\) AND .*id.*=\\s*\\?$").
		WillReturnResult(sqlmock.NewResult(0, 1))

	err := SaveWithVersioning(db, model, nil)
	require.NoError(t, err)
	assert.Equal(t, 8, model.Versioning)
	require.NoError(t, mock.ExpectationsWereMet())
}
