package region

import (
	"context"
	"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"
	regionrepo "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/region"
	mockregion "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/region/mock_region"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

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

	ctrl := gomock.NewController(t)
	regionRepo := mockregion.NewMockRepo(ctrl)
	regionalClient := mockregionalserver.NewMockClient(ctrl)
	service := &service{regionRepo: regionRepo, nextgenCli: regionalClient}
	cloudRegion := activeBYOCRegion()
	request := listNodeSpecsRequest(globalngv1.NodeSpecComponentType_NODE_SPEC_COMPONENT_TYPE_TIKV)
	request.PageSize = 1

	regionRepo.EXPECT().SelectRegion(gomock.Any(), regionrepo.Filter{
		CloudProvider: globalcommonv1.CloudProvider_AWS,
		RegionId:      globalcommonv1.Region_AWS_US_WEST_2,
		ServicePlan:   globalcommonv1.ServicePlan_BYOC,
		TenantId:      42,
	}).Return(cloudRegion, nil).Times(2)
	regionalClient.EXPECT().ListNodeSpecs(
		gomock.Any(),
		cloudRegion,
		&clusterngv1beta.ListNodeSpecsRequest{
			ComponentType: clusterngv1beta.ComponentType_TIKV,
			TenantId:      "42",
		},
	).Return(tikvNodeSpecResponse(), nil).Times(2)

	firstPage, err := service.ListNodeSpecs(context.Background(), request)
	require.Nil(t, err)
	require.EqualValues(t, 2, firstPage.GetTotalSize())
	require.Len(t, firstPage.GetNodeSpecs(), 1)
	require.Equal(t, "4C16G", firstPage.GetNodeSpecs()[0].GetNodeSpecKey())
	require.False(t, firstPage.GetNodeSpecs()[0].GetIsDefault())
	require.Equal(t, []*globalngv1.NodeSpecStorage{
		{Name: "data", DefaultSizeGi: 500, MinSizeGi: 100, MaxSizeGi: 4000},
	}, firstPage.GetNodeSpecs()[0].GetStorages())
	require.NotEmpty(t, firstPage.GetNextPageToken())

	request.PageToken = firstPage.GetNextPageToken()
	secondPage, err := service.ListNodeSpecs(context.Background(), request)
	require.Nil(t, err)
	require.Len(t, secondPage.GetNodeSpecs(), 1)
	require.Equal(t, "8C32G", secondPage.GetNodeSpecs()[0].GetNodeSpecKey())
	require.True(t, secondPage.GetNodeSpecs()[0].GetIsDefault())
	require.Empty(t, secondPage.GetNextPageToken())
	require.Equal(t, globalngv1.NodeCountRuleType_NODE_COUNT_RULE_TYPE_MULTIPLE_OF,
		secondPage.GetNodeCountOptions().GetRule().GetType())
}

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

	ctrl := gomock.NewController(t)
	regionRepo := mockregion.NewMockRepo(ctrl)
	regionalClient := mockregionalserver.NewMockClient(ctrl)
	service := &service{regionRepo: regionRepo, nextgenCli: regionalClient}
	cloudRegion := activeBYOCRegion()
	request := listNodeSpecsRequest(globalngv1.NodeSpecComponentType_NODE_SPEC_COMPONENT_TYPE_TIFLASH)

	regionRepo.EXPECT().SelectRegion(gomock.Any(), gomock.Any()).Return(cloudRegion, nil)
	regionalClient.EXPECT().ListNodeSpecs(
		gomock.Any(),
		cloudRegion,
		&clusterngv1beta.ListNodeSpecsRequest{
			ComponentType: clusterngv1beta.ComponentType_TIFLASH_COMPUTE,
			TenantId:      "42",
		},
	).Return(&clusterngv1beta.ListNodeSpecsResponse{
		NodeSpecOptions: []*clusterngv1beta.NodeSpecOptions{
			regionalNodeSpecOption(8, 32,
				cloudStorageOption(clusterngv1beta.Component_Storage_DATA, 600, 200, 5000),
				localStorageOption(clusterngv1beta.Component_Storage_DATA, 1800),
			),
		},
		NodeCountOptions: &clusterngv1beta.NodeCountOptions{
			DefaultNodeCount: 1,
			MinNodeCount:     1,
			MaxNodeCount:     1,
			Rule: &clusterngv1beta.NodeCountRule{
				Type:  clusterngv1beta.NodeCountRuleType_NODE_COUNT_RULE_TYPE_FIXED,
				Value: 1,
			},
		},
	}, nil)

	response, err := service.ListNodeSpecs(context.Background(), request)
	require.Nil(t, err)
	require.Len(t, response.GetNodeSpecs(), 1)
	require.Equal(t, globalngv1.NodeSpecComponentType_NODE_SPEC_COMPONENT_TYPE_TIFLASH,
		response.GetNodeSpecs()[0].GetComponentType())
	require.Equal(t, []*globalngv1.NodeSpecStorage{
		{Name: "data", DefaultSizeGi: 600, MinSizeGi: 200, MaxSizeGi: 5000},
	}, response.GetNodeSpecs()[0].GetStorages())
}

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

	ctrl := gomock.NewController(t)
	regionRepo := mockregion.NewMockRepo(ctrl)
	regionalClient := mockregionalserver.NewMockClient(ctrl)
	service := &service{regionRepo: regionRepo, nextgenCli: regionalClient}
	cloudRegion := activeBYOCRegion()

	regionRepo.EXPECT().SelectRegion(gomock.Any(), gomock.Any()).Return(cloudRegion, nil)
	regionalClient.EXPECT().ListNodeSpecs(gomock.Any(), cloudRegion, gomock.Any()).Return(
		&clusterngv1beta.ListNodeSpecsResponse{
			NodeSpecOptions: []*clusterngv1beta.NodeSpecOptions{
				regionalNodeSpecOption(8, 32,
					cloudStorageOption(clusterngv1beta.Component_Storage_DATA, 500, 100, 4000),
					cloudStorageOption(clusterngv1beta.Component_Storage_RAFTLOG, 50, 20, 500),
				),
				regionalNodeSpecOption(8, 32,
					cloudStorageOption(clusterngv1beta.Component_Storage_DATA, 500, 100, 4000),
					cloudStorageOption(clusterngv1beta.Component_Storage_RAFTLOG, 50, 20, 500),
				),
			},
			NodeCountOptions: tikvNodeSpecResponse().GetNodeCountOptions(),
		},
		nil,
	)

	_, err := service.ListNodeSpecs(
		context.Background(),
		listNodeSpecsRequest(globalngv1.NodeSpecComponentType_NODE_SPEC_COMPONENT_TYPE_TIKV),
	)
	require.NotNil(t, err)
	require.Equal(t, bizerr.ErrCode(codes.Unavailable), err.ErrCode())
}

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

	ctrl := gomock.NewController(t)
	regionRepo := mockregion.NewMockRepo(ctrl)
	regionalClient := mockregionalserver.NewMockClient(ctrl)
	service := &service{regionRepo: regionRepo, nextgenCli: regionalClient}
	cloudRegion := activeBYOCRegion()

	regionRepo.EXPECT().SelectRegion(gomock.Any(), gomock.Any()).Return(cloudRegion, nil)
	regionalClient.EXPECT().ListNodeSpecs(gomock.Any(), cloudRegion, gomock.Any()).Return(
		&clusterngv1beta.ListNodeSpecsResponse{
			NodeSpecOptions: []*clusterngv1beta.NodeSpecOptions{
				regionalNodeSpecOption(8, 32,
					cloudStorageOption(clusterngv1beta.Component_Storage_DATA, 500, 100, 4000),
					localStorageOption(clusterngv1beta.Component_Storage_RAFTLOG, 100),
				),
			},
			NodeCountOptions: tikvNodeSpecResponse().GetNodeCountOptions(),
		},
		nil,
	)

	response, err := service.ListNodeSpecs(
		context.Background(),
		listNodeSpecsRequest(globalngv1.NodeSpecComponentType_NODE_SPEC_COMPONENT_TYPE_TIKV),
	)

	require.NoError(t, err)
	require.Empty(t, response.GetNodeSpecs())
	require.Zero(t, response.GetTotalSize())
}

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

	tests := []struct {
		name    string
		options []*clusterngv1beta.StorageSizeOptions
	}{
		{
			name: "duplicate recognized cloud name",
			options: []*clusterngv1beta.StorageSizeOptions{
				cloudStorageOption(clusterngv1beta.Component_Storage_DATA, 500, 100, 4000),
				cloudStorageOption(clusterngv1beta.Component_Storage_DATA, 600, 100, 4000),
			},
		},
		{
			name: "default below minimum",
			options: []*clusterngv1beta.StorageSizeOptions{
				cloudStorageOption(clusterngv1beta.Component_Storage_DATA, 99, 100, 4000),
			},
		},
		{
			name: "maximum below default",
			options: []*clusterngv1beta.StorageSizeOptions{
				cloudStorageOption(clusterngv1beta.Component_Storage_DATA, 500, 100, 499),
			},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()

			_, _, err := convertRegionalStorageOptions(
				test.options,
				globalngv1.NodeSpecComponentType_NODE_SPEC_COMPONENT_TYPE_TIFLASH,
			)

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

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

	for _, ruleType := range []clusterngv1beta.NodeCountRuleType{
		clusterngv1beta.NodeCountRuleType_NODE_COUNT_RULE_TYPE_MULTIPLE_OF,
		clusterngv1beta.NodeCountRuleType_NODE_COUNT_RULE_TYPE_FIXED,
	} {
		_, err := convertRegionalNodeCountOptions(&clusterngv1beta.NodeCountOptions{
			DefaultNodeCount: 1,
			MinNodeCount:     1,
			MaxNodeCount:     1,
			Rule: &clusterngv1beta.NodeCountRule{
				Type: ruleType,
			},
		})
		require.Error(t, err)
		require.Equal(t, bizerr.ErrCode(codes.Unavailable), err.ErrCode())
	}
}

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

	ctrl := gomock.NewController(t)
	regionRepo := mockregion.NewMockRepo(ctrl)
	regionalClient := mockregionalserver.NewMockClient(ctrl)
	service := &service{regionRepo: regionRepo, nextgenCli: regionalClient}
	cloudRegion := activeBYOCRegion()
	request := listNodeSpecsRequest(globalngv1.NodeSpecComponentType_NODE_SPEC_COMPONENT_TYPE_TIKV)
	request.PageSize = 1

	regionRepo.EXPECT().SelectRegion(gomock.Any(), gomock.Any()).Return(cloudRegion, nil).Times(2)
	gomock.InOrder(
		regionalClient.EXPECT().ListNodeSpecs(gomock.Any(), cloudRegion, gomock.Any()).Return(tikvNodeSpecResponse(), nil),
		regionalClient.EXPECT().ListNodeSpecs(gomock.Any(), cloudRegion, gomock.Any()).Return(
			&clusterngv1beta.ListNodeSpecsResponse{
				NodeSpecOptions: []*clusterngv1beta.NodeSpecOptions{
					regionalNodeSpecOption(8, 32,
						cloudStorageOption(clusterngv1beta.Component_Storage_DATA, 500, 100, 4000),
						cloudStorageOption(clusterngv1beta.Component_Storage_RAFTLOG, 50, 20, 500),
					),
				},
				NodeCountOptions: tikvNodeSpecResponse().GetNodeCountOptions(),
			},
			nil,
		),
	)

	firstPage, err := service.ListNodeSpecs(context.Background(), request)
	require.Nil(t, err)
	request.PageToken = firstPage.GetNextPageToken()

	_, err = service.ListNodeSpecs(context.Background(), request)
	require.NotNil(t, err)
	require.Equal(t, bizerr.ErrCodeParams, err.ErrCode())
}

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

	service := &service{}
	tests := []struct {
		name    string
		request *globalngv1.ListNodeSpecsRequest
	}{
		{name: "nil request"},
		{name: "missing organization", request: &globalngv1.ListNodeSpecsRequest{}},
		{
			name: "unsupported component",
			request: &globalngv1.ListNodeSpecsRequest{
				OrganizationId:  42,
				CloudProviderId: globalcommonv1.CloudProvider_AWS,
				RegionId:        globalcommonv1.Region_AWS_US_WEST_2,
			},
		},
		{
			name: "negative page size",
			request: func() *globalngv1.ListNodeSpecsRequest {
				request := listNodeSpecsRequest(globalngv1.NodeSpecComponentType_NODE_SPEC_COMPONENT_TYPE_TIKV)
				request.PageSize = -1
				return request
			}(),
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()
			_, err := service.ListNodeSpecs(context.Background(), test.request)
			require.NotNil(t, err)
			require.Equal(t, bizerr.ErrCodeParams, err.ErrCode())
		})
	}
}

func activeBYOCRegion() *entity.Region {
	return &entity.Region{
		Region: &globalcommonv1.Region{
			State:       globalcommonv1.Region_ACTIVE,
			ServicePlan: globalcommonv1.ServicePlan_BYOC,
		},
	}
}

func listNodeSpecsRequest(componentType globalngv1.NodeSpecComponentType) *globalngv1.ListNodeSpecsRequest {
	return &globalngv1.ListNodeSpecsRequest{
		OrganizationId:  42,
		CloudProviderId: globalcommonv1.CloudProvider_AWS,
		RegionId:        globalcommonv1.Region_AWS_US_WEST_2,
		ComponentType:   componentType,
	}
}

func tikvNodeSpecResponse() *clusterngv1beta.ListNodeSpecsResponse {
	return &clusterngv1beta.ListNodeSpecsResponse{
		NodeSpecOptions: []*clusterngv1beta.NodeSpecOptions{
			regionalNodeSpecOption(8, 32,
				cloudStorageOption(clusterngv1beta.Component_Storage_DATA, 500, 100, 4000),
				cloudStorageOption(clusterngv1beta.Component_Storage_RAFTLOG, 50, 20, 500),
				localStorageOption(clusterngv1beta.Component_Storage_DATA, 1900),
			),
			regionalNodeSpecOption(4, 16,
				cloudStorageOption(clusterngv1beta.Component_Storage_RAFTLOG, 50, 20, 500),
				cloudStorageOption(clusterngv1beta.Component_Storage_DATA, 500, 100, 4000),
			),
		},
		NodeCountOptions: &clusterngv1beta.NodeCountOptions{
			DefaultNodeCount: 3,
			MinNodeCount:     3,
			MaxNodeCount:     9,
			Rule: &clusterngv1beta.NodeCountRule{
				Type:  clusterngv1beta.NodeCountRuleType_NODE_COUNT_RULE_TYPE_MULTIPLE_OF,
				Value: 3,
			},
		},
	}
}

func regionalNodeSpecOption(
	cpu uint32,
	memoryGi uint32,
	storageOptions ...*clusterngv1beta.StorageSizeOptions,
) *clusterngv1beta.NodeSpecOptions {
	return &clusterngv1beta.NodeSpecOptions{
		NodeSpec:       &clusterngv1beta.NodeSpec{Cpu: cpu, MemoryGi: memoryGi},
		StorageOptions: storageOptions,
	}
}

func cloudStorageOption(
	name clusterngv1beta.Component_Storage_Name,
	defaultSizeGi uint32,
	minSizeGi uint32,
	maxSizeGi uint32,
) *clusterngv1beta.StorageSizeOptions {
	return &clusterngv1beta.StorageSizeOptions{
		Name:          name,
		Medium:        clusterngv1beta.StorageMedium_STORAGE_MEDIUM_CLOUD,
		DefaultSizeGi: defaultSizeGi,
		MinSizeGi:     minSizeGi,
		MaxSizeGi:     maxSizeGi,
	}
}

func localStorageOption(
	name clusterngv1beta.Component_Storage_Name,
	sizeGi uint32,
) *clusterngv1beta.StorageSizeOptions {
	return &clusterngv1beta.StorageSizeOptions{
		Name:          name,
		Medium:        clusterngv1beta.StorageMedium_STORAGE_MEDIUM_LOCAL,
		DefaultSizeGi: sizeGi,
		MinSizeGi:     sizeGi,
		MaxSizeGi:     sizeGi,
	}
}
