package resourcepool

import (
	"testing"

	"github.com/stretchr/testify/require"
	clusterngv1beta "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"go.uber.org/mock/gomock"
	"google.golang.org/grpc/codes"

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	globalngv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	mockregionalserver "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/adapter/regionalserver/mock_regionalserver"
	"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"
	mockregion "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/region/mock_region"
	mockresourcepool "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/resourcepool/mock_resourcepool"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

func TestFullResourcePoolProfilePreservesDurableLifecycleAndProjectsByocRuntimeDetail(t *testing.T) {
	t.Parallel()

	ctrl := gomock.NewController(t)
	regionRepo := mockregion.NewMockRepo(ctrl)
	regionalCli := mockregionalserver.NewMockClient(ctrl)
	clusterRepo := mockcluster.NewMockRepo(ctrl)
	resourcePoolRepo := mockresourcepool.NewMockRepo(ctrl)
	strategyRepo := mockresourcepool.NewMockStrategyRepo(ctrl)
	svc := &serviceImpl{
		regionRepo:               regionRepo,
		regionalCli:              regionalCli,
		clusterRepo:              clusterRepo,
		resourcePoolRepo:         resourcePoolRepo,
		resourcePoolStrategyRepo: strategyRepo,
	}

	rp, cloudRegion := byocRuntimeTestPool(globalngv1.ResourcePool_MODIFYING)
	strategyRepo.EXPECT().Query(gomock.Any(), gomock.Any()).Return(nil, nil)
	regionRepo.EXPECT().Get(gomock.Any(), rp.RegionId).Return(cloudRegion, nil)
	regionalCli.EXPECT().GetSharedPool(gomock.Any(), cloudRegion, gomock.Any()).Return(
		&clusterngv1beta.SharedPool{
			Phase:    clusterngv1beta.SharedPool_AVAILABLE,
			Provider: &clusterngv1beta.Provider{NodeZones: []string{"us-west-2a", "us-west-2b", "us-west-2c"}},
			ComponentGroups: []*clusterngv1beta.Component{
				byocRuntimeComponent(clusterngv1beta.ComponentType_TIKV, 8, 32, 3,
					byocRuntimeStorage(clusterngv1beta.Component_Storage_DATA, 500),
					byocRuntimeStorage(clusterngv1beta.Component_Storage_RAFTLOG, 40)),
				byocRuntimeComponent(clusterngv1beta.ComponentType_TIFLASH_WRITE, 16, 64, 2,
					byocRuntimeStorage(clusterngv1beta.Component_Storage_DATA, 500)),
				byocRuntimeComponent(clusterngv1beta.ComponentType_TIFLASH_COMPUTE, 16, 64, 1,
					byocRuntimeStorage(clusterngv1beta.Component_Storage_DATA, 500)),
			},
		}, nil)
	gomock.InOrder(
		regionalCli.EXPECT().GetSharedPoolProvisionConfiguration(gomock.Any(), cloudRegion, gomock.Any()).Return(
			&clusterngv1beta.SharedPoolProvisionConfiguration{O11YClusterCidr: "10.20.1.1/16"}, nil),
		regionalCli.EXPECT().GetSharedPoolVcpusUsage(gomock.Any(), cloudRegion, gomock.Any()).Return(
			&clusterngv1beta.GetSharedPoolVcpusUsageResponse{VcpusUsed: 64}, nil),
	)
	clusterRepo.EXPECT().Query(gomock.Any(), gomock.Any()).Return(
		[]*entity.Cluster{{Cluster: &globalngv1.Cluster{ClusterId: 300}}}, nil)

	profile, err := svc.fullResourcePoolProfile(t.Context(), rp)

	require.NoError(t, err)
	require.EqualValues(t, 8, profile.GetCapacity().GetTikv().GetNodeSpec().GetCpu())
	require.EqualValues(t, 3, profile.GetCapacity().GetTikv().GetNodeCount())
	require.EqualValues(t, 1, profile.GetCapacity().GetTiflash().GetNodeCount())
	require.EqualValues(t, 64, profile.GetProvisionedDatabaseVcpus())
	require.Equal(t, "10.20.0.0/16", profile.GetO11YClusterCidr())
	require.Equal(t, []string{"us-west-2a", "us-west-2b", "us-west-2c"}, profile.GetResourcePool().GetZoneConfig().GetNodeZones())
	require.EqualValues(t, 1, profile.GetResourcePool().GetInstanceCount())
	require.Equal(t, globalngv1.ResourcePool_MODIFYING, profile.GetResourcePool().GetLifecycleState())
}

func TestFillByocRuntimeProfileSkipsVcpuUsageWhenRuntimeMayBeAbsent(t *testing.T) {
	t.Parallel()

	for _, state := range []globalngv1.ResourcePool_LifecycleState{
		globalngv1.ResourcePool_CREATING,
		globalngv1.ResourcePool_FAILED,
		globalngv1.ResourcePool_PAUSING,
		globalngv1.ResourcePool_PAUSED,
		globalngv1.ResourcePool_RESUMING,
	} {
		t.Run(state.String(), func(t *testing.T) {
			t.Parallel()

			ctrl := gomock.NewController(t)
			regionalCli := mockregionalserver.NewMockClient(ctrl)
			svc := &serviceImpl{regionalCli: regionalCli}
			_, cloudRegion := byocRuntimeTestPool(state)
			profile := &entity.ResourcePoolProfile{ResourcePool: &globalngv1.ResourcePool{
				Id:             100,
				LifecycleState: state,
			}}
			sharedPool := &clusterngv1beta.SharedPool{ComponentGroups: []*clusterngv1beta.Component{
				byocRuntimeComponent(clusterngv1beta.ComponentType_TIKV, 8, 32, 3),
			}}

			regionalCli.EXPECT().GetSharedPoolProvisionConfiguration(gomock.Any(), cloudRegion, gomock.Any()).Return(
				&clusterngv1beta.SharedPoolProvisionConfiguration{O11YClusterCidr: "10.20.1.1/16"}, nil)

			err := svc.fillByocRuntimeProfile(t.Context(), cloudRegion, profile, sharedPool)

			require.NoError(t, err)
			require.EqualValues(t, 3, profile.GetCapacity().GetTikv().GetNodeCount())
			require.Equal(t, "10.20.0.0/16", profile.GetO11YClusterCidr())
			require.Nil(t, profile.ProvisionedDatabaseVcpus)
		})
	}
}

func TestFillByocRuntimeProfileKeepsO11YCIDRWhenCapacityMayBeAbsent(t *testing.T) {
	t.Parallel()

	ctrl := gomock.NewController(t)
	regionalCli := mockregionalserver.NewMockClient(ctrl)
	svc := &serviceImpl{regionalCli: regionalCli}
	_, cloudRegion := byocRuntimeTestPool(globalngv1.ResourcePool_CREATING)
	profile := &entity.ResourcePoolProfile{ResourcePool: &globalngv1.ResourcePool{
		Id:             100,
		LifecycleState: globalngv1.ResourcePool_CREATING,
	}}
	sharedPool := &clusterngv1beta.SharedPool{}

	regionalCli.EXPECT().GetSharedPoolProvisionConfiguration(gomock.Any(), cloudRegion, gomock.Any()).Return(
		&clusterngv1beta.SharedPoolProvisionConfiguration{O11YClusterCidr: "10.20.1.1/16"}, nil)

	err := svc.fillByocRuntimeProfile(t.Context(), cloudRegion, profile, sharedPool)

	require.NoError(t, err)
	require.Equal(t, "10.20.0.0/16", profile.GetO11YClusterCidr())
	require.Nil(t, profile.GetCapacity())
	require.Nil(t, profile.ProvisionedDatabaseVcpus)
}

func TestFillByocRuntimeProfileReturnsCapacityAndO11YCIDRWhenVcpuUsageIsUnavailable(t *testing.T) {
	t.Parallel()

	ctrl := gomock.NewController(t)
	regionalCli := mockregionalserver.NewMockClient(ctrl)
	svc := &serviceImpl{regionalCli: regionalCli}
	_, cloudRegion := byocRuntimeTestPool(globalngv1.ResourcePool_ACTIVE)
	staleProvisionedVcpus := int64(99)
	profile := &entity.ResourcePoolProfile{
		ResourcePool: &globalngv1.ResourcePool{
			Id:             100,
			LifecycleState: globalngv1.ResourcePool_ACTIVE,
		},
		ProvisionedDatabaseVcpus: &staleProvisionedVcpus,
	}
	sharedPool := &clusterngv1beta.SharedPool{ComponentGroups: []*clusterngv1beta.Component{
		byocRuntimeComponent(clusterngv1beta.ComponentType_TIKV, 8, 32, 3),
	}}
	usageErr := bizerr.New(bizerr.ErrCodeRegionalAPI, "vCPU usage unavailable")

	gomock.InOrder(
		regionalCli.EXPECT().GetSharedPoolProvisionConfiguration(gomock.Any(), cloudRegion, gomock.Any()).Return(
			&clusterngv1beta.SharedPoolProvisionConfiguration{O11YClusterCidr: "10.20.1.1/16"}, nil),
		regionalCli.EXPECT().GetSharedPoolVcpusUsage(gomock.Any(), cloudRegion, gomock.Any()).Return(nil, usageErr),
	)

	err := svc.fillByocRuntimeProfile(t.Context(), cloudRegion, profile, sharedPool)

	require.NoError(t, err)
	require.EqualValues(t, 3, profile.GetCapacity().GetTikv().GetNodeCount())
	require.Equal(t, "10.20.0.0/16", profile.GetO11YClusterCidr())
	require.Nil(t, profile.ProvisionedDatabaseVcpus)
}

func TestFullResourcePoolProfilePreservesCreatingWhenRuntimeIsMissing(t *testing.T) {
	t.Parallel()

	ctrl := gomock.NewController(t)
	regionRepo := mockregion.NewMockRepo(ctrl)
	regionalCli := mockregionalserver.NewMockClient(ctrl)
	clusterRepo := mockcluster.NewMockRepo(ctrl)
	strategyRepo := mockresourcepool.NewMockStrategyRepo(ctrl)
	svc := &serviceImpl{
		regionRepo:               regionRepo,
		regionalCli:              regionalCli,
		clusterRepo:              clusterRepo,
		resourcePoolStrategyRepo: strategyRepo,
	}

	rp, cloudRegion := byocRuntimeTestPool(globalngv1.ResourcePool_CREATING)
	strategyRepo.EXPECT().Query(gomock.Any(), gomock.Any()).Return(nil, nil)
	regionRepo.EXPECT().Get(gomock.Any(), rp.RegionId).Return(cloudRegion, nil)
	regionalCli.EXPECT().GetSharedPool(gomock.Any(), cloudRegion, gomock.Any()).Return(
		nil, bizerr.New(bizerr.ErrCodeRecordNotFound, "shared pool not found"))
	clusterRepo.EXPECT().Query(gomock.Any(), gomock.Any()).Return(nil, nil)

	profile, err := svc.fullResourcePoolProfile(t.Context(), rp)

	require.NoError(t, err)
	require.Nil(t, profile.GetCapacity())
	require.Empty(t, profile.GetO11YClusterCidr())
	require.Equal(t, globalngv1.ResourcePool_CREATING, profile.GetResourcePool().GetLifecycleState())
}

func TestFullResourcePoolProfilePreservesCreatingWhenRegionalPoolIsUnavailable(t *testing.T) {
	t.Parallel()

	ctrl := gomock.NewController(t)
	regionRepo := mockregion.NewMockRepo(ctrl)
	regionalCli := mockregionalserver.NewMockClient(ctrl)
	clusterRepo := mockcluster.NewMockRepo(ctrl)
	strategyRepo := mockresourcepool.NewMockStrategyRepo(ctrl)
	svc := &serviceImpl{
		regionRepo:               regionRepo,
		regionalCli:              regionalCli,
		clusterRepo:              clusterRepo,
		resourcePoolStrategyRepo: strategyRepo,
	}

	rp, cloudRegion := byocRuntimeTestPool(globalngv1.ResourcePool_CREATING)
	strategyRepo.EXPECT().Query(gomock.Any(), gomock.Any()).Return(nil, nil)
	regionRepo.EXPECT().Get(gomock.Any(), rp.RegionId).Return(cloudRegion, nil)
	regionalCli.EXPECT().GetSharedPool(gomock.Any(), cloudRegion, gomock.Any()).Return(
		&clusterngv1beta.SharedPool{Phase: clusterngv1beta.SharedPool_UNAVAILABLE}, nil)
	regionalCli.EXPECT().GetSharedPoolProvisionConfiguration(gomock.Any(), cloudRegion, gomock.Any()).Return(
		&clusterngv1beta.SharedPoolProvisionConfiguration{O11YClusterCidr: "10.20.1.1/16"}, nil)
	clusterRepo.EXPECT().Query(gomock.Any(), gomock.Any()).Return(nil, nil)

	profile, err := svc.fullResourcePoolProfile(t.Context(), rp)

	require.NoError(t, err)
	require.Nil(t, profile.GetCapacity())
	require.Equal(t, "10.20.0.0/16", profile.GetO11YClusterCidr())
	require.Equal(t, globalngv1.ResourcePool_CREATING, profile.GetResourcePool().GetLifecycleState())
}

func TestFullResourcePoolProfileReturnsUnavailableForMissingActiveRuntime(t *testing.T) {
	t.Parallel()

	ctrl := gomock.NewController(t)
	regionRepo := mockregion.NewMockRepo(ctrl)
	regionalCli := mockregionalserver.NewMockClient(ctrl)
	strategyRepo := mockresourcepool.NewMockStrategyRepo(ctrl)
	svc := &serviceImpl{
		regionRepo:               regionRepo,
		regionalCli:              regionalCli,
		resourcePoolStrategyRepo: strategyRepo,
	}

	rp, cloudRegion := byocRuntimeTestPool(globalngv1.ResourcePool_ACTIVE)
	strategyRepo.EXPECT().Query(gomock.Any(), gomock.Any()).Return(nil, nil)
	regionRepo.EXPECT().Get(gomock.Any(), rp.RegionId).Return(cloudRegion, nil)
	regionalCli.EXPECT().GetSharedPool(gomock.Any(), cloudRegion, gomock.Any()).Return(
		nil, bizerr.New(bizerr.ErrCodeRecordNotFound, "shared pool not found"))

	profile, err := svc.fullResourcePoolProfile(t.Context(), rp)

	require.Nil(t, profile)
	require.Error(t, err)
	require.Equal(t, bizerr.ErrCode(codes.Unavailable), err.ErrCode())
}

func TestFullResourcePoolProfileDoesNotHideRegionalErrorForPausedRuntime(t *testing.T) {
	t.Parallel()

	ctrl := gomock.NewController(t)
	regionRepo := mockregion.NewMockRepo(ctrl)
	regionalCli := mockregionalserver.NewMockClient(ctrl)
	strategyRepo := mockresourcepool.NewMockStrategyRepo(ctrl)
	svc := &serviceImpl{
		regionRepo:               regionRepo,
		regionalCli:              regionalCli,
		resourcePoolStrategyRepo: strategyRepo,
	}

	rp, cloudRegion := byocRuntimeTestPool(globalngv1.ResourcePool_PAUSED)
	strategyRepo.EXPECT().Query(gomock.Any(), gomock.Any()).Return(nil, nil)
	regionRepo.EXPECT().Get(gomock.Any(), rp.RegionId).Return(cloudRegion, nil)
	regionalErr := bizerr.New(bizerr.ErrCodeRegionalAPI, "shared pool response is malformed")
	regionalCli.EXPECT().GetSharedPool(gomock.Any(), cloudRegion, gomock.Any()).Return(nil, regionalErr)

	profile, err := svc.fullResourcePoolProfile(t.Context(), rp)

	require.Nil(t, profile)
	require.Error(t, err)
	require.Equal(t, bizerr.ErrCode(codes.Unavailable), err.ErrCode())
	require.ErrorContains(t, err, "shared pool response is malformed")
}

func byocRuntimeTestPool(state globalngv1.ResourcePool_LifecycleState) (*entity.ResourcePool, *entity.Region) {
	return &entity.ResourcePool{
			ResourcePool: &globalngv1.ResourcePool{
				Id:             100,
				ProviderId:     globalcommonv1.CloudProvider_AWS,
				RegionId:       globalcommonv1.Region_AWS_US_WEST_2,
				OwnerType:      globalngv1.ResourcePool_ORGANIZATION,
				OwnerId:        42,
				ServicePlan:    globalcommonv1.ServicePlan_BYOC,
				LifecycleState: state,
			},
			RegionId:        10,
			CloudProviderId: 20,
		}, &entity.Region{
			Region: &globalcommonv1.Region{
				Id:          globalcommonv1.Region_AWS_US_WEST_2,
				ServicePlan: globalcommonv1.ServicePlan_BYOC,
				State:       globalcommonv1.Region_ACTIVE,
			},
			RegionID:   10,
			ProviderID: 20,
		}
}

func byocRuntimeComponent(
	componentType clusterngv1beta.ComponentType,
	cpu, memoryGi, nodeCount uint32,
	storages ...*clusterngv1beta.Component_Storage,
) *clusterngv1beta.Component {
	return &clusterngv1beta.Component{
		ComponentType: componentType,
		NodeQuota: &clusterngv1beta.NodeQuota{
			NodeSpec:  &clusterngv1beta.NodeSpec{Cpu: cpu, MemoryGi: memoryGi},
			NodeCount: nodeCount,
		},
		Storages: storages,
	}
}

func byocRuntimeStorage(name clusterngv1beta.Component_Storage_Name, sizeGi uint32) *clusterngv1beta.Component_Storage {
	return &clusterngv1beta.Component_Storage{Name: name, SizeGi: sizeGi}
}
