package resourcepool

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"
	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/nextgen-global-service/pkg/config"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

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

	valid, err := parseCanonicalIPv4CIDR("10.2.0.0/16")
	require.NoError(t, err)
	require.Equal(t, "10.2.0.0/16", valid.canonical)
	require.True(t, isPrivateCIDR(valid))

	_, err = parseCanonicalIPv4CIDR("10.2.1.1/16")
	require.Error(t, err)

	crossesPrivateBoundary, err := parseCanonicalIPv4CIDR("10.0.0.0/7")
	require.NoError(t, err)
	require.False(t, isPrivateCIDR(crossesPrivateBoundary))

	ipv6, err := parseCanonicalIPv4CIDR("fd00::/16")
	require.Error(t, err)
	require.False(t, isPrivateCIDR(ipv6))
}

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

	ctrl := gomock.NewController(t)
	regionRepo := mockregion.NewMockRepo(ctrl)
	cloudRegion := cidrTestRegion()
	regionRepo.EXPECT().SelectRegion(gomock.Any(), gomock.Any()).Return(cloudRegion, nil)
	service := &serviceImpl{regionRepo: regionRepo}

	response, err := service.ValidateResourcePoolCidr(t.Context(), &globalngv1.ValidateResourcePoolCidrRequest{
		OrganizationId:  42,
		CloudProviderId: globalcommonv1.CloudProvider_AWS,
		RegionId:        globalcommonv1.Region_AWS_US_WEST_2,
		Cidr:            "10.0.1.1/16",
	})

	require.NoError(t, err)
	require.Equal(t, "10.0.1.1/16", response.GetCidr())
	require.Nil(t, response.SupportedNodeCount)
	require.Equal(t, []globalngv1.ResourcePoolCidrValidationFinding_Code{
		globalngv1.ResourcePoolCidrValidationFinding_INVALID_CIDR,
	}, findingCodes(response.GetBlockingFindings()))
}

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

	ctrl := gomock.NewController(t)
	regionRepo := mockregion.NewMockRepo(ctrl)
	regionRepo.EXPECT().SelectRegion(gomock.Any(), gomock.Any()).Return(cidrTestRegion(), nil)
	service := &serviceImpl{regionRepo: regionRepo}

	response, err := service.ValidateResourcePoolCidr(t.Context(), &globalngv1.ValidateResourcePoolCidrRequest{
		OrganizationId:  42,
		CloudProviderId: globalcommonv1.CloudProvider_AWS,
		RegionId:        globalcommonv1.Region_AWS_US_WEST_2,
		Cidr:            "8.8.0.0/16",
	})

	require.NoError(t, err)
	require.Nil(t, response.SupportedNodeCount)
	require.Len(t, response.GetBlockingFindings(), 1)
	require.Equal(t, globalngv1.ResourcePoolCidrValidationFinding_CIDR_NOT_PRIVATE,
		response.GetBlockingFindings()[0].GetCode())
	require.Equal(t, "The CIDR must be fully contained within a private IPv4 address range.",
		response.GetBlockingFindings()[0].GetMessage())
}

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

	service := &serviceImpl{}
	for _, cidr := range []string{"", "  \t\n"} {
		_, err := service.ValidateResourcePoolCidr(t.Context(), &globalngv1.ValidateResourcePoolCidrRequest{
			OrganizationId:  42,
			CloudProviderId: globalcommonv1.CloudProvider_AWS,
			RegionId:        globalcommonv1.Region_AWS_US_WEST_2,
			Cidr:            cidr,
		})
		require.Error(t, err)
		require.Equal(t, bizerr.ErrCodeParams, err.ErrCode())
	}
}

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

	tests := []struct {
		name     string
		response *clusterngv1beta.EvaluateSharedPoolNetworkCapacityResponse
	}{
		{name: "nil response"},
		{name: "supported without capacity", response: &clusterngv1beta.EvaluateSharedPoolNetworkCapacityResponse{Supported: true}},
		{name: "unsupported with capacity", response: &clusterngv1beta.EvaluateSharedPoolNetworkCapacityResponse{SupportedNodeCount: 1}},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()
			ctrl := gomock.NewController(t)
			regional := mockregionalserver.NewMockClient(ctrl)
			regional.EXPECT().EvaluateSharedPoolNetworkCapacity(gomock.Any(), gomock.Any(), gomock.Any()).Return(test.response, nil)
			service := &serviceImpl{regionalCli: regional}

			_, _, err := service.evaluateResourcePoolNetworkCapacity(
				t.Context(), cidrTestRegion(), 18, globalngv1.ResourcePool_REGIONAL,
			)

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

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

	ctrl := gomock.NewController(t)
	regionRepo := mockregion.NewMockRepo(ctrl)
	poolRepo := mockresourcepool.NewMockRepo(ctrl)
	regional := mockregionalserver.NewMockClient(ctrl)
	cloudRegion := cidrTestRegion()
	service := &serviceImpl{
		cfg: &config.Config{ResourcePoolNetwork: config.ResourcePoolNetworkConfig{
			ReservedCIDRs: []string{"10.0.192.0/18"},
		}},
		regionRepo:       regionRepo,
		resourcePoolRepo: poolRepo,
		regionalCli:      regional,
	}

	regionRepo.EXPECT().SelectRegion(gomock.Any(), gomock.Any()).Return(cloudRegion, nil)
	poolRepo.EXPECT().ListLiveBYOCOrganizationResourcePoolsInRegion(
		gomock.Any(),
		uint64(42),
		globalcommonv1.CloudProvider_AWS,
		globalcommonv1.Region_AWS_US_WEST_2,
	).Return([]*entity.ResourcePool{{
		ResourcePool: &globalngv1.ResourcePool{
			Id:         100,
			Name:       "existing",
			Cidr:       "10.0.0.0/17",
			ProviderId: globalcommonv1.CloudProvider_AWS,
			RegionId:   globalcommonv1.Region_AWS_US_WEST_2,
		},
	}}, nil)
	regional.EXPECT().GetSharedPoolProvisionConfiguration(gomock.Any(), cloudRegion, gomock.Any()).Return(
		&clusterngv1beta.SharedPoolProvisionConfiguration{O11YClusterCidr: "10.0.128.0/18"}, nil)
	regional.EXPECT().EvaluateSharedPoolNetworkCapacity(gomock.Any(), cloudRegion, gomock.Any()).DoAndReturn(
		func(_ context.Context, _ *entity.Region, request *clusterngv1beta.EvaluateSharedPoolNetworkCapacityRequest) (*clusterngv1beta.EvaluateSharedPoolNetworkCapacityResponse, bizerr.BizError) {
			require.NotNil(t, request.CidrPrefixLength)
			require.EqualValues(t, 16, request.GetCidrPrefixLength())
			require.Equal(t, clusterngv1beta.CreateSharedPoolRequest_InitSharedPoolConfig_ZONAL, request.GetHighAvailabilityType())
			return &clusterngv1beta.EvaluateSharedPoolNetworkCapacityResponse{Supported: true, SupportedNodeCount: 128}, nil
		})

	response, err := service.ValidateResourcePoolCidr(t.Context(), &globalngv1.ValidateResourcePoolCidrRequest{
		OrganizationId:       42,
		CloudProviderId:      globalcommonv1.CloudProvider_AWS,
		RegionId:             globalcommonv1.Region_AWS_US_WEST_2,
		Cidr:                 " 10.0.0.0/16 ",
		HighAvailabilityType: globalngv1.ResourcePool_ZONAL,
	})

	require.NoError(t, err)
	require.Equal(t, "10.0.0.0/16", response.GetCidr())
	require.NotNil(t, response.SupportedNodeCount)
	require.EqualValues(t, 128, response.GetSupportedNodeCount())
	require.Equal(t, []globalngv1.ResourcePoolCidrValidationFinding_Code{
		globalngv1.ResourcePoolCidrValidationFinding_CIDR_OVERLAPS_RESOURCE_POOL,
		globalngv1.ResourcePoolCidrValidationFinding_CIDR_OVERLAPS_O11Y,
		globalngv1.ResourcePoolCidrValidationFinding_CIDR_OVERLAPS_RESERVED_NETWORK,
	}, findingCodes(response.GetBlockingFindings()))
}

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

	ctrl := gomock.NewController(t)
	regionRepo := mockregion.NewMockRepo(ctrl)
	poolRepo := mockresourcepool.NewMockRepo(ctrl)
	regional := mockregionalserver.NewMockClient(ctrl)
	cloudRegion := cidrTestRegion()
	service := &serviceImpl{regionRepo: regionRepo, resourcePoolRepo: poolRepo, regionalCli: regional}

	regionRepo.EXPECT().SelectRegion(gomock.Any(), gomock.Any()).Return(cloudRegion, nil)
	poolRepo.EXPECT().ListLiveBYOCOrganizationResourcePoolsInRegion(
		gomock.Any(),
		uint64(42),
		globalcommonv1.CloudProvider_AWS,
		globalcommonv1.Region_AWS_US_WEST_2,
	).Return(nil, nil)
	regional.EXPECT().GetSharedPoolProvisionConfiguration(gomock.Any(), cloudRegion, gomock.Any()).Return(
		&clusterngv1beta.SharedPoolProvisionConfiguration{O11YClusterCidr: "192.168.0.0/16"}, nil)
	gomock.InOrder(
		regional.EXPECT().EvaluateSharedPoolNetworkCapacity(gomock.Any(), cloudRegion, gomock.Any()).DoAndReturn(
			func(_ context.Context, _ *entity.Region, request *clusterngv1beta.EvaluateSharedPoolNetworkCapacityRequest) (*clusterngv1beta.EvaluateSharedPoolNetworkCapacityResponse, bizerr.BizError) {
				require.EqualValues(t, 16, request.GetCidrPrefixLength())
				return &clusterngv1beta.EvaluateSharedPoolNetworkCapacityResponse{}, nil
			}),
		regional.EXPECT().EvaluateSharedPoolNetworkCapacity(gomock.Any(), cloudRegion, gomock.Any()).DoAndReturn(
			func(_ context.Context, _ *entity.Region, request *clusterngv1beta.EvaluateSharedPoolNetworkCapacityRequest) (*clusterngv1beta.EvaluateSharedPoolNetworkCapacityResponse, bizerr.BizError) {
				require.EqualValues(t, 17, request.GetCidrPrefixLength())
				return &clusterngv1beta.EvaluateSharedPoolNetworkCapacityResponse{Supported: true, SupportedNodeCount: 64}, nil
			}),
	)

	response, err := service.PlanResourcePoolCidr(t.Context(), &globalngv1.PlanResourcePoolCidrRequest{
		OrganizationId:  42,
		CloudProviderId: globalcommonv1.CloudProvider_AWS,
		RegionId:        globalcommonv1.Region_AWS_US_WEST_2,
	})

	require.NoError(t, err)
	require.Equal(t, globalngv1.PlanResourcePoolCidrResponse_AVAILABLE, response.GetRecommendationStatus())
	require.Equal(t, "10.0.0.0/17", response.GetRecommendation().GetCidr())
	require.EqualValues(t, 64, response.GetRecommendation().GetSupportedNodeCount())
	require.Equal(t, []*globalngv1.ResourcePoolCidrPrefixRange{{
		MinPrefixLength: 16,
		MaxPrefixLength: 22,
	}}, response.GetPolicy().GetAllowedPrefixRanges())
}

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

	ctrl := gomock.NewController(t)
	regionRepo := mockregion.NewMockRepo(ctrl)
	poolRepo := mockresourcepool.NewMockRepo(ctrl)
	regional := mockregionalserver.NewMockClient(ctrl)
	cloudRegion := cidrTestRegion()
	service := &serviceImpl{
		cfg: &config.Config{ResourcePoolNetwork: config.ResourcePoolNetworkConfig{
			AWSRecommendationStartPrefixLength: 18,
			AWSMinPrefixLength:                 17,
			AWSMaxPrefixLength:                 19,
		}},
		regionRepo:       regionRepo,
		resourcePoolRepo: poolRepo,
		regionalCli:      regional,
	}

	regionRepo.EXPECT().SelectRegion(gomock.Any(), gomock.Any()).Return(cloudRegion, nil)
	poolRepo.EXPECT().ListLiveBYOCOrganizationResourcePoolsInRegion(
		gomock.Any(),
		uint64(42),
		globalcommonv1.CloudProvider_AWS,
		globalcommonv1.Region_AWS_US_WEST_2,
	).Return(nil, nil)
	regional.EXPECT().GetSharedPoolProvisionConfiguration(gomock.Any(), cloudRegion, gomock.Any()).Return(
		&clusterngv1beta.SharedPoolProvisionConfiguration{O11YClusterCidr: "192.168.0.0/16"}, nil)
	gomock.InOrder(
		regional.EXPECT().EvaluateSharedPoolNetworkCapacity(gomock.Any(), cloudRegion, gomock.Any()).DoAndReturn(
			func(_ context.Context, _ *entity.Region, request *clusterngv1beta.EvaluateSharedPoolNetworkCapacityRequest) (*clusterngv1beta.EvaluateSharedPoolNetworkCapacityResponse, bizerr.BizError) {
				require.EqualValues(t, 18, request.GetCidrPrefixLength())
				return &clusterngv1beta.EvaluateSharedPoolNetworkCapacityResponse{}, nil
			}),
		regional.EXPECT().EvaluateSharedPoolNetworkCapacity(gomock.Any(), cloudRegion, gomock.Any()).DoAndReturn(
			func(_ context.Context, _ *entity.Region, request *clusterngv1beta.EvaluateSharedPoolNetworkCapacityRequest) (*clusterngv1beta.EvaluateSharedPoolNetworkCapacityResponse, bizerr.BizError) {
				require.EqualValues(t, 19, request.GetCidrPrefixLength())
				return &clusterngv1beta.EvaluateSharedPoolNetworkCapacityResponse{Supported: true, SupportedNodeCount: 64}, nil
			}),
	)

	response, err := service.PlanResourcePoolCidr(t.Context(), &globalngv1.PlanResourcePoolCidrRequest{
		OrganizationId:  42,
		CloudProviderId: globalcommonv1.CloudProvider_AWS,
		RegionId:        globalcommonv1.Region_AWS_US_WEST_2,
	})

	require.NoError(t, err)
	require.Equal(t, globalngv1.PlanResourcePoolCidrResponse_AVAILABLE, response.GetRecommendationStatus())
	require.Equal(t, "10.0.0.0/19", response.GetRecommendation().GetCidr())
	require.EqualValues(t, 64, response.GetRecommendation().GetSupportedNodeCount())
	require.Equal(t, []*globalngv1.ResourcePoolCidrPrefixRange{{
		MinPrefixLength: 17,
		MaxPrefixLength: 19,
	}}, response.GetPolicy().GetAllowedPrefixRanges())
}

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

	tests := []struct {
		name       string
		service    *serviceImpl
		providerID globalcommonv1.CloudProvider_Name
		want       []int
	}{
		{
			name:       "AWS uses the default start prefix",
			service:    &serviceImpl{},
			providerID: globalcommonv1.CloudProvider_AWS,
			want:       []int{16, 17, 18, 19, 20, 21, 22},
		},
		{
			name: "AWS uses the configured start and maximum prefixes",
			service: &serviceImpl{cfg: &config.Config{ResourcePoolNetwork: config.ResourcePoolNetworkConfig{
				AWSRecommendationStartPrefixLength: 18,
				AWSMaxPrefixLength:                 20,
			}}},
			providerID: globalcommonv1.CloudProvider_AWS,
			want:       []int{18, 19, 20},
		},
		{
			name: "non AWS keeps the existing prefix when the configured minimum changes",
			service: &serviceImpl{cfg: &config.Config{ResourcePoolNetwork: config.ResourcePoolNetworkConfig{
				AWSRecommendationStartPrefixLength: 18,
				AWSMinPrefixLength:                 18,
				AWSMaxPrefixLength:                 20,
			}}},
			providerID: globalcommonv1.CloudProvider_GCP,
			want:       []int{16},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()
			require.Equal(t, test.want, test.service.recommendationPrefixLengths(test.providerID))
		})
	}
}

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

	ctrl := gomock.NewController(t)
	poolRepo := mockresourcepool.NewMockRepo(ctrl)
	service := &serviceImpl{resourcePoolRepo: poolRepo}
	poolRepo.EXPECT().LockOrganizationNamespace(gomock.Any(), uint64(42)).Return(nil)
	poolRepo.EXPECT().HasLiveNormalizedName(gomock.Any(), uint64(42), "new-pool", uint64(0)).Return(false, nil)
	poolRepo.EXPECT().ListLiveBYOCOrganizationResourcePoolsInRegion(
		gomock.Any(),
		uint64(42),
		globalcommonv1.CloudProvider_AWS,
		globalcommonv1.Region_AWS_US_WEST_2,
	).Return([]*entity.ResourcePool{{
		ResourcePool: &globalngv1.ResourcePool{
			Id:         100,
			Name:       "existing",
			Cidr:       "10.0.0.0/17",
			ProviderId: globalcommonv1.CloudProvider_AWS,
			RegionId:   globalcommonv1.Region_AWS_US_WEST_2,
		},
	}}, nil)

	err := service.lockAndValidateOrganizationNamespaceForCreate(t.Context(), &globalngv1.ResourcePool{
		Name:        "new-pool",
		OwnerType:   globalngv1.ResourcePool_ORGANIZATION,
		OwnerId:     42,
		ServicePlan: globalcommonv1.ServicePlan_BYOC,
		ProviderId:  globalcommonv1.CloudProvider_AWS,
		RegionId:    globalcommonv1.Region_AWS_US_WEST_2,
		Cidr:        "10.0.0.0/16",
	})

	var failure *CIDRValidationFailure
	require.ErrorAs(t, err, &failure)
	require.Equal(t, globalngv1.ResourcePoolCidrValidationFinding_CIDR_OVERLAPS_RESOURCE_POOL, failure.Findings[0].GetCode())
}

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

	pool := &globalngv1.ResourcePool{
		OwnerType:   globalngv1.ResourcePool_ORGANIZATION,
		OwnerId:     42,
		ServicePlan: globalcommonv1.ServicePlan_BYOC,
	}
	require.NoError(t, normalizeResourcePoolHighAvailability(pool))
	require.Equal(t, globalngv1.ResourcePool_REGIONAL, pool.GetHighAvailabilityType())

	pool.HighAvailabilityType = globalngv1.ResourcePool_ZONAL
	require.NoError(t, normalizeResourcePoolHighAvailability(pool))
	require.Equal(t, globalngv1.ResourcePool_ZONAL, pool.GetHighAvailabilityType())

	pool.HighAvailabilityType = globalngv1.ResourcePool_HighAvailabilityType(99)
	require.Error(t, normalizeResourcePoolHighAvailability(pool))
}

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

	tests := []struct {
		name  string
		input globalngv1.ResourcePool_HighAvailabilityType
		want  globalngv1.ResourcePool_HighAvailabilityType
		valid bool
	}{
		{name: "unspecified defaults to regional", want: globalngv1.ResourcePool_REGIONAL, valid: true},
		{name: "regional", input: globalngv1.ResourcePool_REGIONAL, want: globalngv1.ResourcePool_REGIONAL, valid: true},
		{name: "zonal", input: globalngv1.ResourcePool_ZONAL, want: globalngv1.ResourcePool_ZONAL, valid: true},
		{name: "invalid", input: globalngv1.ResourcePool_HighAvailabilityType(99)},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()
			got, err := normalizeResourcePoolCIDRHighAvailability(test.input)
			if !test.valid {
				require.Error(t, err)
				return
			}
			require.Nil(t, err)
			require.Equal(t, test.want, got)
		})
	}
}

func cidrTestRegion() *entity.Region {
	return &entity.Region{Region: &globalcommonv1.Region{
		Id:              globalcommonv1.Region_AWS_US_WEST_2,
		CloudProviderId: globalcommonv1.CloudProvider_AWS,
		State:           globalcommonv1.Region_ACTIVE,
		ServicePlan:     globalcommonv1.ServicePlan_BYOC,
		ServiceAddress:  "https://regional.example.com",
	}}
}

func findingCodes(findings []*globalngv1.ResourcePoolCidrValidationFinding) []globalngv1.ResourcePoolCidrValidationFinding_Code {
	result := make([]globalngv1.ResourcePoolCidrValidationFinding_Code, 0, len(findings))
	for _, finding := range findings {
		result = append(result, finding.GetCode())
	}
	return result
}
