package tidbcluster

import (
	"context"
	"path/filepath"
	"sync"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/pkg/storage/mysql/schema"
	"github.com/tidbcloud/cluster-service-ng/pkg/storage/repo"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type testDBProvider struct {
	db *gorm.DB
}

func (p testDBProvider) ReadDB(context.Context) *gorm.DB {
	return p.db
}

func (p testDBProvider) WriteDB(context.Context) *gorm.DB {
	return p.db
}

func TestSaveCreatesNewTiDBCluster(t *testing.T) {
	gdb := newTestDB(t, ":memory:")
	clusterRepo := newTestRepo(gdb)
	cluster := newTestTiDBCluster()

	require.NoError(t, clusterRepo.Save(context.Background(), cluster))

	var saved schema.TidbCluster
	require.NoError(t, gdb.Where("id = ?", "cluster-1").First(&saved).Error)
	require.Equal(t, "tenant-1", saved.TenantID)
	require.Equal(t, "shared-pool-1", saved.SharedPoolID)
	require.Equal(t, int64(2), saved.RUMin)
	require.Equal(t, int64(4), saved.RUMax)
	require.Equal(t, "gateway-1", saved.GatewayIdentity)
	require.Equal(t, int32(1), saved.RuVersion)
	require.Equal(t, apiv1.CapacityMode_TOPOLOGY_DRIVEN.String(), saved.CapacityMode)
	require.Equal(t, int64(1), saved.TemplateVersion)
}

func TestSaveUpdatesExistingTiDBClusterWithoutResettingTemplateMetadata(t *testing.T) {
	gdb := newTestDB(t, ":memory:")
	clusterRepo := newTestRepo(gdb)
	createdAt := time.Date(2026, 5, 25, 10, 0, 0, 0, time.UTC)
	require.NoError(t, gdb.Create(&schema.TidbCluster{
		ID:              "cluster-1",
		TenantID:        "tenant-old",
		SharedPoolID:    "shared-pool-old",
		RUMin:           1,
		RUMax:           1,
		CapacityMode:    apiv1.CapacityMode_RCU_DRIVEN.String(),
		AuditLogConfig:  "{}",
		GatewayIdentity: "gateway-old",
		RuVersion:       1,
		TemplateVersion: 7,
		CreatedAt:       createdAt,
		UpdatedAt:       createdAt,
	}).Error)
	require.NoError(t, gdb.Create(&schema.TidbCluster{
		ID:              "cluster-other",
		TenantID:        "tenant-other",
		SharedPoolID:    "shared-pool-other",
		RUMin:           1,
		RUMax:           1,
		CapacityMode:    apiv1.CapacityMode_RCU_DRIVEN.String(),
		AuditLogConfig:  "{}",
		GatewayIdentity: "gateway-other",
		RuVersion:       1,
		TemplateVersion: 9,
		CreatedAt:       createdAt,
		UpdatedAt:       createdAt,
	}).Error)

	cluster := newTestTiDBCluster()
	cluster.TenantId = "tenant-new"
	cluster.SharedPoolId = "shared-pool-new"
	cluster.GatewayIdentity = "gateway-new"
	cluster.RequestUnit = &entity.RequestUnit{Min: 8, Max: 16}
	cluster.RuVersion = 3

	require.NoError(t, clusterRepo.Save(context.Background(), cluster))

	var saved schema.TidbCluster
	require.NoError(t, gdb.Where("id = ?", "cluster-1").First(&saved).Error)
	require.Equal(t, "tenant-new", saved.TenantID)
	require.Equal(t, "shared-pool-new", saved.SharedPoolID)
	require.Equal(t, int64(8), saved.RUMin)
	require.Equal(t, int64(16), saved.RUMax)
	require.Equal(t, "gateway-new", saved.GatewayIdentity)
	require.Equal(t, int32(3), saved.RuVersion)
	require.Equal(t, int64(7), saved.TemplateVersion)
	require.WithinDuration(t, createdAt, saved.CreatedAt, time.Second)
}

func TestSaveRepeatedCreateIsIdempotent(t *testing.T) {
	gdb := newTestDB(t, ":memory:")
	clusterRepo := newTestRepo(gdb)
	cluster := newTestTiDBCluster()

	require.NoError(t, clusterRepo.Save(context.Background(), cluster))
	cluster.TenantId = "tenant-2"
	cluster.RequestUnit = &entity.RequestUnit{Min: 4, Max: 8}
	require.NoError(t, clusterRepo.Save(context.Background(), cluster))

	var count int64
	require.NoError(t, gdb.Model(&schema.TidbCluster{}).Where("id = ?", "cluster-1").Count(&count).Error)
	require.Equal(t, int64(1), count)
	var saved schema.TidbCluster
	require.NoError(t, gdb.Where("id = ?", "cluster-1").First(&saved).Error)
	require.Equal(t, "tenant-2", saved.TenantID)
	require.Equal(t, int64(4), saved.RUMin)
	require.Equal(t, int64(8), saved.RUMax)
	require.Equal(t, int64(1), saved.TemplateVersion)
}

func TestSaveConcurrentFirstWriteIsIdempotent(t *testing.T) {
	dbPath := filepath.Join(t.TempDir(), "tidbcluster.db")
	gdb := newTestDB(t, "file:"+dbPath+"?cache=shared&mode=rwc&_busy_timeout=5000&_journal_mode=WAL")
	sqlDB, err := gdb.DB()
	require.NoError(t, err)
	// SQLite returns immediate table-lock errors for concurrent writers here.
	// The production idempotency guarantee comes from the generated upsert.
	sqlDB.SetMaxOpenConns(1)

	clusterRepo := newTestRepo(gdb)
	const workers = 16
	start := make(chan struct{})
	var wg sync.WaitGroup
	errs := make(chan error, workers)
	for i := 0; i < workers; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			<-start
			errs <- clusterRepo.Save(context.Background(), newTestTiDBCluster())
		}()
	}
	close(start)
	wg.Wait()
	close(errs)

	for err := range errs {
		require.NoError(t, err)
	}
	var count int64
	require.NoError(t, gdb.Model(&schema.TidbCluster{}).Where("id = ?", "cluster-1").Count(&count).Error)
	require.Equal(t, int64(1), count)
}

func newTestDB(t *testing.T, dsn string) *gorm.DB {
	t.Helper()
	gdb, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, gdb.AutoMigrate(&schema.TidbCluster{}))
	return gdb
}

func newTestRepo(gdb *gorm.DB) repo.TiDBClusterRepo {
	return NewClusterRepo(testDBProvider{db: gdb}, nil, nil)
}

func newTestTiDBCluster() *entity.TiDBCluster {
	return &entity.TiDBCluster{
		Id:              "cluster-1",
		TenantId:        "tenant-1",
		SharedPoolId:    "shared-pool-1",
		GatewayIdentity: "gateway-1",
		RequestUnit: &entity.RequestUnit{
			Min: 2,
			Max: 4,
		},
		AuditLogSetting: &entity.AuditLogSetting{},
		RuVersion:       1,
		CapacityMode:    apiv1.CapacityMode_TOPOLOGY_DRIVEN,
	}
}
