package updatetest

import (
	"context"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/mock/gomock"

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	globalngv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/entity"
	mockcluster "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/cluster/mock_cluster"
	mockreservedcapacitytemplate "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/reservedcapacitytemplate/mock_reservedcapacitytemplate"
	mockresourcepool "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/resourcepool/mock_resourcepool"
	clustersvc "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/service/cluster"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/service/workflow/common"
	mockworkflow "github.com/tidbcloud/tidb-management-service/utilities/workflow/mock_workflow"
)

// mockTransactioner executes the callback directly (no real DB transaction).
type mockTransactioner struct{}

func (m *mockTransactioner) Transaction(ctx context.Context, fn func(ctx context.Context) error) error {
	return fn(ctx)
}

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

	clusterRepo := mockcluster.NewMockRepo(ctrl)
	resourcePoolRepo := mockresourcepool.NewMockRepo(ctrl)
	wf := mockworkflow.NewMockWorkflow(ctrl)

	svc := clustersvc.NewServiceForTest(wf, &mockTransactioner{}, clusterRepo, resourcePoolRepo)

	existingCluster := &entity.Cluster{
		Cluster: &globalngv1.Cluster{
			ClusterId:      12345,
			OrganizationId: 100,
			MinRcu:         entity.DefaultMinRcu,
			MaxRcu:         20000,
			RuVersion:      1,
			State:          globalngv1.Cluster_AVAILABLE,
			SharedPoolId:   "303",
		},
		ProjectId: 200,
		RegionId:  1,
	}

	clusterRepo.EXPECT().Get(gomock.Any(), uint64(12345), false).Return(existingCluster, nil)
	resourcePoolRepo.EXPECT().Load(gomock.Any(), uint64(303), false).Return(activeResourcePool(), nil)
	resourcePoolRepo.EXPECT().LoadForUpdate(gomock.Any(), uint64(303), false).Return(activeResourcePool(), nil)
	clusterRepo.EXPECT().Save(gomock.Any(), gomock.Any()).DoAndReturn(
		func(_ context.Context, c *entity.Cluster) error {
			assert.Equal(t, int32(2), c.RuVersion, "ru_version should be updated to 2")
			assert.Equal(t, globalngv1.Cluster_MODIFYING, c.State, "state should be MODIFYING")
			assert.Equal(t, int64(entity.DefaultMinRcu), c.MinRcu, "MinRcu should remain unchanged")
			assert.Equal(t, int64(20000), c.MaxRcu, "MaxRcu should remain unchanged")
			return nil
		},
	)
	wf.EXPECT().RunDagWithBizId(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)

	req := &globalngv1.UpdateClusterRequest{
		Cluster: &globalngv1.Cluster{
			ClusterId: 12345,
			RuVersion: 2,
		},
	}

	result, bizErr := svc.Update(context.Background(), req)
	require.Nil(t, bizErr)
	require.NotNil(t, result)
	assert.Equal(t, int32(2), result.RuVersion)
	assert.Equal(t, int64(entity.DefaultMinRcu), result.MinRcu, "MinRcu should remain unchanged")
	assert.Equal(t, int64(20000), result.MaxRcu, "MaxRcu should remain unchanged")
}

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

	clusterRepo := mockcluster.NewMockRepo(ctrl)
	resourcePoolRepo := mockresourcepool.NewMockRepo(ctrl)

	svc := clustersvc.NewServiceForTest(nil, &mockTransactioner{}, clusterRepo, resourcePoolRepo)

	existingCluster := &entity.Cluster{
		Cluster: &globalngv1.Cluster{
			ClusterId:      12345,
			OrganizationId: 100,
			MinRcu:         entity.DefaultMinRcu,
			MaxRcu:         20000,
			RuVersion:      1,
			State:          globalngv1.Cluster_AVAILABLE,
			SharedPoolId:   "303",
		},
		ProjectId: 200,
	}

	clusterRepo.EXPECT().Get(gomock.Any(), uint64(12345), false).Return(existingCluster, nil)
	resourcePoolRepo.EXPECT().Load(gomock.Any(), uint64(303), false).Return(activeResourcePool(), nil)

	req := &globalngv1.UpdateClusterRequest{
		Cluster: &globalngv1.Cluster{
			ClusterId: 12345,
			RuVersion: 1,
		},
	}

	result, bizErr := svc.Update(context.Background(), req)
	require.Nil(t, bizErr)
	require.NotNil(t, result)
	assert.Equal(t, globalngv1.Cluster_AVAILABLE, result.State)
}

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

	clusterRepo := mockcluster.NewMockRepo(ctrl)
	resourcePoolRepo := mockresourcepool.NewMockRepo(ctrl)

	svc := clustersvc.NewServiceForTest(nil, &mockTransactioner{}, clusterRepo, resourcePoolRepo)

	existingCluster := &entity.Cluster{
		Cluster: &globalngv1.Cluster{
			ClusterId:      12345,
			OrganizationId: 100,
			MinRcu:         entity.DefaultMinRcu,
			MaxRcu:         20000,
			RuVersion:      1,
			State:          globalngv1.Cluster_AVAILABLE,
			SharedPoolId:   "303",
		},
		ProjectId: 200,
	}

	clusterRepo.EXPECT().Get(gomock.Any(), uint64(12345), false).Return(existingCluster, nil)
	resourcePoolRepo.EXPECT().Load(gomock.Any(), uint64(303), false).Return(activeResourcePool(), nil)

	req := &globalngv1.UpdateClusterRequest{
		Cluster: &globalngv1.Cluster{
			ClusterId: 12345,
			RuVersion: 0,
		},
	}

	result, bizErr := svc.Update(context.Background(), req)
	require.Nil(t, bizErr)
	require.NotNil(t, result)
	assert.Equal(t, int32(1), result.RuVersion, "ru_version should remain unchanged")
}

func TestUpdate_RejectsHighAvailabilityChange(t *testing.T) {
	ctrl := gomock.NewController(t)
	clusterRepo := mockcluster.NewMockRepo(ctrl)
	resourcePoolRepo := mockresourcepool.NewMockRepo(ctrl)
	svc := clustersvc.NewServiceForTest(nil, &mockTransactioner{}, clusterRepo, resourcePoolRepo)

	existingCluster := &entity.Cluster{
		Cluster: &globalngv1.Cluster{
			ClusterId:            12345,
			OrganizationId:       100,
			ServicePlan:          globalcommonv1.ServicePlan_Essential_V2,
			HighAvailabilityType: globalngv1.Cluster_ZONAL,
			State:                globalngv1.Cluster_AVAILABLE,
			SharedPoolId:         "303",
		},
	}
	resourcePool := activeResourcePool()
	resourcePool.ServicePlan = globalcommonv1.ServicePlan_Essential_V2
	resourcePool.HighAvailabilityType = globalngv1.ResourcePool_ZONAL
	clusterRepo.EXPECT().Get(gomock.Any(), uint64(12345), false).Return(existingCluster, nil)
	resourcePoolRepo.EXPECT().Load(gomock.Any(), uint64(303), false).Return(resourcePool, nil)

	result, bizErr := svc.Update(context.Background(), &globalngv1.UpdateClusterRequest{
		Cluster: &globalngv1.Cluster{
			ClusterId:            12345,
			HighAvailabilityType: globalngv1.Cluster_REGIONAL,
		},
	})

	require.Nil(t, result)
	require.ErrorContains(t, bizErr, "high_availability_type is immutable")
}

func TestUpdate_AcceptsUnchangedHighAvailability(t *testing.T) {
	ctrl := gomock.NewController(t)
	clusterRepo := mockcluster.NewMockRepo(ctrl)
	resourcePoolRepo := mockresourcepool.NewMockRepo(ctrl)
	svc := clustersvc.NewServiceForTest(nil, &mockTransactioner{}, clusterRepo, resourcePoolRepo)

	existingCluster := &entity.Cluster{
		Cluster: &globalngv1.Cluster{
			ClusterId:            12345,
			OrganizationId:       100,
			ServicePlan:          globalcommonv1.ServicePlan_Essential_V2,
			HighAvailabilityType: globalngv1.Cluster_ZONAL,
			State:                globalngv1.Cluster_AVAILABLE,
			SharedPoolId:         "303",
		},
	}
	resourcePool := activeResourcePool()
	resourcePool.ServicePlan = globalcommonv1.ServicePlan_Essential_V2
	resourcePool.HighAvailabilityType = globalngv1.ResourcePool_ZONAL
	clusterRepo.EXPECT().Get(gomock.Any(), uint64(12345), false).Return(existingCluster, nil)
	resourcePoolRepo.EXPECT().Load(gomock.Any(), uint64(303), false).Return(resourcePool, nil)

	result, bizErr := svc.Update(context.Background(), &globalngv1.UpdateClusterRequest{
		Cluster: &globalngv1.Cluster{
			ClusterId:            12345,
			HighAvailabilityType: globalngv1.Cluster_ZONAL,
		},
	})

	require.Nil(t, bizErr)
	require.Same(t, existingCluster, result)
}

func TestUpdate_AcceptsStoredHighAvailabilityWhenPoolDiffers(t *testing.T) {
	ctrl := gomock.NewController(t)
	clusterRepo := mockcluster.NewMockRepo(ctrl)
	resourcePoolRepo := mockresourcepool.NewMockRepo(ctrl)
	svc := clustersvc.NewServiceForTest(nil, &mockTransactioner{}, clusterRepo, resourcePoolRepo)

	existingCluster := &entity.Cluster{
		Cluster: &globalngv1.Cluster{
			ClusterId:            12345,
			OrganizationId:       100,
			ServicePlan:          globalcommonv1.ServicePlan_Essential_V2,
			HighAvailabilityType: globalngv1.Cluster_REGIONAL,
			State:                globalngv1.Cluster_AVAILABLE,
			SharedPoolId:         "303",
		},
	}
	resourcePool := activeResourcePool()
	resourcePool.ServicePlan = globalcommonv1.ServicePlan_Essential_V2
	resourcePool.HighAvailabilityType = globalngv1.ResourcePool_ZONAL
	clusterRepo.EXPECT().Get(gomock.Any(), uint64(12345), false).Return(existingCluster, nil)
	resourcePoolRepo.EXPECT().Load(gomock.Any(), uint64(303), false).Return(resourcePool, nil)

	result, bizErr := svc.Update(context.Background(), &globalngv1.UpdateClusterRequest{
		Cluster: &globalngv1.Cluster{
			ClusterId:            12345,
			HighAvailabilityType: globalngv1.Cluster_REGIONAL,
		},
	})

	require.Nil(t, bizErr)
	require.Same(t, existingCluster, result)
}

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

	clusterRepo := mockcluster.NewMockRepo(ctrl)
	resourcePoolRepo := mockresourcepool.NewMockRepo(ctrl)

	svc := clustersvc.NewServiceForTest(nil, &mockTransactioner{}, clusterRepo, resourcePoolRepo)

	existingCluster := &entity.Cluster{
		Cluster: &globalngv1.Cluster{
			ClusterId:      12345,
			OrganizationId: 100,
			MinRcu:         entity.DefaultMinRcu,
			MaxRcu:         20000,
			RuVersion:      1,
			State:          globalngv1.Cluster_MODIFYING,
			SharedPoolId:   "303",
		},
		ProjectId: 200,
	}

	clusterRepo.EXPECT().Get(gomock.Any(), uint64(12345), false).Return(existingCluster, nil)
	resourcePoolRepo.EXPECT().Load(gomock.Any(), uint64(303), false).Return(activeResourcePool(), nil)

	req := &globalngv1.UpdateClusterRequest{
		Cluster: &globalngv1.Cluster{
			ClusterId: 12345,
			RuVersion: 2,
		},
	}

	_, bizErr := svc.Update(context.Background(), req)
	require.NotNil(t, bizErr)
	assert.Contains(t, bizErr.Error(), "cluster is not available")
}

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

	clusterRepo := mockcluster.NewMockRepo(ctrl)
	templateRepo := mockreservedcapacitytemplate.NewMockRepo(ctrl)
	resourcePoolRepo := mockresourcepool.NewMockRepo(ctrl)
	wf := mockworkflow.NewMockWorkflow(ctrl)

	svc := clustersvc.NewServiceForTest(wf, &mockTransactioner{}, clusterRepo, resourcePoolRepo, templateRepo)

	existingCluster := &entity.Cluster{
		Cluster: &globalngv1.Cluster{
			ClusterId:                  12345,
			OrganizationId:             100,
			ServicePlan:                globalcommonv1.ServicePlan_Premium_Reserved,
			ReservedCapacityTemplateId: 1,
			State:                      globalngv1.Cluster_AVAILABLE,
			SharedPoolId:               "303",
		},
		ProjectId: 200,
	}

	targetTemplate := &entity.ReservedCapacityTemplate{
		Id:             2,
		OrganizationId: 100,
		Status:         globalngv1.ReservedCapacityTemplate_ACTIVE,
		Scope:          globalngv1.ReservedCapacityTemplate_ORG_PRIVATE,
	}

	clusterRepo.EXPECT().Get(gomock.Any(), uint64(12345), false).Return(existingCluster, nil)
	resourcePoolRepo.EXPECT().Load(gomock.Any(), uint64(303), false).Return(activeResourcePool(), nil)
	resourcePoolRepo.EXPECT().LoadForUpdate(gomock.Any(), uint64(303), false).Return(activeResourcePool(), nil)
	templateRepo.EXPECT().Load(gomock.Any(), uint64(2), false).Return(targetTemplate, nil)
	clusterRepo.EXPECT().Save(gomock.Any(), gomock.Any()).DoAndReturn(
		func(_ context.Context, c *entity.Cluster) error {
			assert.Equal(t, uint64(2), c.GetReservedCapacityTemplateId())
			assert.Equal(t, globalngv1.Cluster_MODIFYING, c.GetState())
			return nil
		},
	)
	wf.EXPECT().RunDagWithBizId(
		gomock.Any(),
		common.UpdateClusterTopology,
		gomock.Any(),
		gomock.Any(),
		gomock.Any(),
		gomock.Any(),
	).Return(nil)

	result, bizErr := svc.Update(context.Background(), &globalngv1.UpdateClusterRequest{
		Cluster: &globalngv1.Cluster{
			ClusterId:                  12345,
			ReservedCapacityTemplateId: 2,
		},
	})
	require.Nil(t, bizErr)
	require.NotNil(t, result)
	assert.Equal(t, uint64(2), result.GetReservedCapacityTemplateId())
	assert.Equal(t, globalngv1.Cluster_MODIFYING, result.GetState())
}

func activeResourcePool() *entity.ResourcePool {
	return &entity.ResourcePool{ResourcePool: &globalngv1.ResourcePool{
		Id:             303,
		LifecycleState: globalngv1.ResourcePool_ACTIVE,
	}}
}
