package cluster

import (
	"context"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"google.golang.org/grpc/codes"
	"google.golang.org/protobuf/types/known/emptypb"

	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/adapter/billing"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/adapter/remoteencryptor"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/entity"
	clusterrepo "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/cluster"
	rcuquotarepo "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/rcuquota"
	regionrepo "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/region"
	encryptionkeysvc "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/service/encryptionkey"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

func TestCreateValidateOnlyEssentialV2RejectsWhenSharedPoolMissing(t *testing.T) {
	ctx := t.Context()
	poolSvc := &fakeCreateResourcePoolService{
		pickSharedErr: bizerr.New(bizerr.ErrCodeRecordNotFound, "shared resource pool not found"),
	}
	svc := &serviceImpl{
		clusterRepo:      &validateOnlyClusterRepo{},
		regionRepo:       &validateOnlyRegionRepo{},
		rcuQuotaRepo:     &validateOnlyRcuQuotaRepo{},
		billingCli:       &validateOnlyBillingClient{},
		encryptor:        &validateOnlyEncryptor{},
		resourcePoolSvc:  poolSvc,
		encryptionKeySvc: &validateOnlyEncryptionKeyService{},
	}

	cluster, err := svc.Create(ctx, &globalngv1.CreateClusterRequest{
		ValidateOnly: true,
		Cluster: &globalngv1.Cluster{
			ClusterId:       100,
			DisplayName:     "essential-v2-validate-only",
			OrganizationId:  200,
			ProjectId:       300,
			CloudProviderId: globalcommonv1.CloudProvider_AWS,
			RegionId:        globalcommonv1.Region_AWS_US_EAST_1,
			ServicePlan:     globalcommonv1.ServicePlan_Essential_V2,
			MaxRcu:          20_000,
		},
	})

	require.Nil(t, cluster)
	require.NotNil(t, err)
	assert.Equal(t, bizerr.ErrCode(codes.ResourceExhausted), err.ErrCode())
	assert.Contains(t, err.Error(), "server resources are insufficient")
	assert.Equal(t, 1, poolSvc.pickSharedCalls)
	assert.Equal(t, 0, poolSvc.getOrCreateDefaultCalls)
}

func TestCreateValidateOnlyEssentialV2SucceedsWhenSharedPoolExists(t *testing.T) {
	ctx := t.Context()
	poolSvc := &fakeCreateResourcePoolService{
		sharedPool: &entity.ResourcePool{ResourcePool: &globalngv1.ResourcePool{
			Id:                   101,
			Name:                 "essential-shared",
			ProviderId:           globalcommonv1.CloudProvider_AWS,
			RegionId:             globalcommonv1.Region_AWS_US_EAST_1,
			ServicePlan:          globalcommonv1.ServicePlan_Essential_V2,
			OwnerType:            globalngv1.ResourcePool_SHARED,
			LifecycleState:       globalngv1.ResourcePool_ACTIVE,
			HighAvailabilityType: globalngv1.ResourcePool_ZONAL,
		}},
	}
	svc := &serviceImpl{
		clusterRepo:      &validateOnlyClusterRepo{},
		regionRepo:       &validateOnlyRegionRepo{},
		rcuQuotaRepo:     &validateOnlyRcuQuotaRepo{},
		billingCli:       &validateOnlyBillingClient{},
		encryptor:        &validateOnlyEncryptor{},
		resourcePoolSvc:  poolSvc,
		encryptionKeySvc: &validateOnlyEncryptionKeyService{},
	}

	cluster, err := svc.Create(ctx, &globalngv1.CreateClusterRequest{
		ValidateOnly: true,
		Cluster: &globalngv1.Cluster{
			ClusterId:       101,
			DisplayName:     "essential-v2-validate-only",
			OrganizationId:  200,
			ProjectId:       300,
			CloudProviderId: globalcommonv1.CloudProvider_AWS,
			RegionId:        globalcommonv1.Region_AWS_US_EAST_1,
			ServicePlan:     globalcommonv1.ServicePlan_Essential_V2,
			MaxRcu:          20_000,
		},
	})

	require.Nil(t, err)
	require.NotNil(t, cluster)
	assert.Equal(t, "101", cluster.GetSharedPoolId())
	assert.Equal(t, "essential-shared", cluster.GetResourcePoolDisplayName())
	assert.Equal(t, globalngv1.Cluster_ZONAL, cluster.GetHighAvailabilityType())
	assert.Equal(t, 1, poolSvc.pickSharedCalls)
	assert.Equal(t, 0, poolSvc.getOrCreateDefaultCalls)
}

func TestCreateValidateOnlyWithDefaultKeyDoesNotRequireProviderIdentity(t *testing.T) {
	ctx := t.Context()
	encryptionKeySvc := &validateOnlyEncryptionKeyService{}
	svc := &serviceImpl{
		clusterRepo:      &validateOnlyClusterRepo{},
		regionRepo:       &validateOnlyRegionRepo{},
		rcuQuotaRepo:     &validateOnlyRcuQuotaRepo{},
		billingCli:       &validateOnlyBillingClient{},
		encryptor:        &validateOnlyEncryptor{},
		resourcePoolSvc:  &fakeCreateResourcePoolService{},
		encryptionKeySvc: encryptionKeySvc,
	}

	cluster, err := svc.Create(ctx, &globalngv1.CreateClusterRequest{
		ValidateOnly: true,
		Cluster: &globalngv1.Cluster{
			ClusterId:       10007669285045832905,
			DisplayName:     "yj-cmek-test",
			OrganizationId:  30061,
			ProjectId:       3202083,
			CloudProviderId: globalcommonv1.CloudProvider_AWS,
			RegionId:        globalcommonv1.Region_AWS_US_EAST_1,
			ServicePlan:     globalcommonv1.ServicePlan_Premium,
			MaxRcu:          20000,
			DualLayerDataEncryption: &globalngv1.Cluster_DualLayerDataEncryption{
				Key: &globalngv1.Cluster_DualLayerDataEncryption_DefaultKey{
					DefaultKey: &emptypb.Empty{},
				},
			},
		},
	})

	require.Nil(t, err)
	require.NotNil(t, cluster)
	assert.Equal(t, globalngv1.Cluster_CREATING, cluster.State)
	assert.EqualValues(t, 0, cluster.EncryptionKeyId)
	assert.Equal(t, 0, encryptionKeySvc.defaultKeyCalls)
	assert.Equal(t, 0, encryptionKeySvc.cmekCalls)
}

func TestCreateValidateOnlyRejectsDefaultPoolWhilePausing(t *testing.T) {
	poolSvc := &fakeCreateResourcePoolService{
		findDefaultPool: &entity.ResourcePool{ResourcePool: &globalngv1.ResourcePool{
			Id:             101,
			ProviderId:     globalcommonv1.CloudProvider_AWS,
			RegionId:       globalcommonv1.Region_AWS_US_EAST_1,
			ServicePlan:    globalcommonv1.ServicePlan_Premium,
			OwnerType:      globalngv1.ResourcePool_ORGANIZATION,
			OwnerId:        30061,
			LifecycleState: globalngv1.ResourcePool_PAUSING,
		}},
	}
	svc := &serviceImpl{
		clusterRepo:      &validateOnlyClusterRepo{},
		regionRepo:       &validateOnlyRegionRepo{},
		rcuQuotaRepo:     &validateOnlyRcuQuotaRepo{},
		billingCli:       &validateOnlyBillingClient{},
		encryptor:        &validateOnlyEncryptor{},
		resourcePoolSvc:  poolSvc,
		encryptionKeySvc: &validateOnlyEncryptionKeyService{},
	}

	cluster, err := svc.Create(t.Context(), &globalngv1.CreateClusterRequest{
		ValidateOnly: true,
		Cluster: &globalngv1.Cluster{
			ClusterId:       10007669285045832905,
			DisplayName:     "validate-only-pausing-pool",
			OrganizationId:  30061,
			ProjectId:       3202083,
			CloudProviderId: globalcommonv1.CloudProvider_AWS,
			RegionId:        globalcommonv1.Region_AWS_US_EAST_1,
			ServicePlan:     globalcommonv1.ServicePlan_Premium,
			MaxRcu:          20000,
		},
	})

	require.Nil(t, cluster)
	require.NotNil(t, err)
	assert.Equal(t, bizerr.ErrCodeGeneralBusiness, err.ErrCode())
	assert.Contains(t, err.Error(), "resource pool is not available for TiDB creation")
	assert.Equal(t, 1, poolSvc.findDefaultCalls)
}

func TestCreateValidateOnlyWithCmekValidatesAccess(t *testing.T) {
	for _, tt := range []struct {
		name         string
		sharedPoolID string
	}{
		{
			name:         "existing shared pool",
			sharedPoolID: "1001",
		},
		{
			name:         "default shared pool",
			sharedPoolID: "",
		},
	} {
		t.Run(tt.name, func(t *testing.T) {
			ctx := t.Context()
			cmek := validateOnlyAwsCmek()
			encryptionKeySvc := &validateOnlyEncryptionKeyService{}
			poolSvc := &fakeCreateResourcePoolService{}
			expectedSharedPoolID := tt.sharedPoolID
			if tt.sharedPoolID == "" {
				expectedSharedPoolID = "101"
				poolSvc.findDefaultPool = &entity.ResourcePool{ResourcePool: &globalngv1.ResourcePool{
					Id:             101,
					ProviderId:     globalcommonv1.CloudProvider_AWS,
					RegionId:       globalcommonv1.Region_AWS_US_EAST_1,
					ServicePlan:    globalcommonv1.ServicePlan_Premium,
					OwnerType:      globalngv1.ResourcePool_ORGANIZATION,
					OwnerId:        30061,
					LifecycleState: globalngv1.ResourcePool_ACTIVE,
				}}
			}
			svc := &serviceImpl{
				clusterRepo:      &validateOnlyClusterRepo{},
				regionRepo:       &validateOnlyRegionRepo{},
				rcuQuotaRepo:     &validateOnlyRcuQuotaRepo{},
				billingCli:       &validateOnlyBillingClient{},
				encryptor:        &validateOnlyEncryptor{},
				resourcePoolSvc:  poolSvc,
				encryptionKeySvc: encryptionKeySvc,
			}

			cluster, err := svc.Create(ctx, &globalngv1.CreateClusterRequest{
				ValidateOnly: true,
				Cluster: &globalngv1.Cluster{
					ClusterId:       10007669285045832905,
					DisplayName:     "yj-cmek-test",
					OrganizationId:  30061,
					ProjectId:       3202083,
					CloudProviderId: globalcommonv1.CloudProvider_AWS,
					RegionId:        globalcommonv1.Region_AWS_US_EAST_1,
					ServicePlan:     globalcommonv1.ServicePlan_Premium,
					MaxRcu:          20000,
					SharedPoolId:    tt.sharedPoolID,
					DualLayerDataEncryption: &globalngv1.Cluster_DualLayerDataEncryption{
						Key: &globalngv1.Cluster_DualLayerDataEncryption_Cmek{
							Cmek: cmek,
						},
					},
				},
			})

			require.Nil(t, err)
			require.NotNil(t, cluster)
			assert.Equal(t, globalngv1.Cluster_CREATING, cluster.State)
			assert.EqualValues(t, 0, cluster.EncryptionKeyId)
			assert.Equal(t, 0, encryptionKeySvc.defaultKeyCalls)
			assert.Equal(t, 0, encryptionKeySvc.cmekCalls)
			assert.Equal(t, 1, encryptionKeySvc.validateCmekCalls)
			assert.Same(t, cmek, encryptionKeySvc.validatedCmek)
			assert.Equal(t, expectedSharedPoolID, encryptionKeySvc.validatedSharedPoolID)
		})
	}
}

func TestCreateValidateOnlyWithCmekDoesNotResolveMissingPool(t *testing.T) {
	encryptionKeySvc := &validateOnlyEncryptionKeyService{}
	poolSvc := &fakeCreateResourcePoolService{}
	svc := &serviceImpl{
		clusterRepo:      &validateOnlyClusterRepo{},
		regionRepo:       &validateOnlyRegionRepo{},
		rcuQuotaRepo:     &validateOnlyRcuQuotaRepo{},
		billingCli:       &validateOnlyBillingClient{},
		encryptor:        &validateOnlyEncryptor{},
		resourcePoolSvc:  poolSvc,
		encryptionKeySvc: encryptionKeySvc,
	}

	cluster, err := svc.Create(t.Context(), &globalngv1.CreateClusterRequest{
		ValidateOnly: true,
		Cluster: &globalngv1.Cluster{
			ClusterId:       10007669285045832905,
			DisplayName:     "cmek-without-default-pool",
			OrganizationId:  30061,
			ProjectId:       3202083,
			CloudProviderId: globalcommonv1.CloudProvider_AWS,
			RegionId:        globalcommonv1.Region_AWS_US_EAST_1,
			ServicePlan:     globalcommonv1.ServicePlan_Premium,
			MaxRcu:          20000,
			DualLayerDataEncryption: &globalngv1.Cluster_DualLayerDataEncryption{
				Key: &globalngv1.Cluster_DualLayerDataEncryption_Cmek{
					Cmek: validateOnlyAwsCmek(),
				},
			},
		},
	})

	require.Nil(t, cluster)
	require.NotNil(t, err)
	assert.Equal(t, bizerr.ErrCodeParams, err.ErrCode())
	assert.Contains(t, err.Error(), "resource pool is required for CMEK validation")
	assert.Equal(t, 1, poolSvc.findDefaultCalls)
	assert.Equal(t, 0, encryptionKeySvc.validateCmekCalls)
}

func TestCreateValidateOnlyWithCmekReturnsAccessValidationError(t *testing.T) {
	ctx := t.Context()
	encryptionKeySvc := &validateOnlyEncryptionKeyService{
		validateCmekErr: bizerr.New(bizerr.ErrCodeParams, "account not matched"),
	}
	poolSvc := &fakeCreateResourcePoolService{
		findDefaultPool: &entity.ResourcePool{ResourcePool: &globalngv1.ResourcePool{
			Id:             101,
			ProviderId:     globalcommonv1.CloudProvider_AWS,
			RegionId:       globalcommonv1.Region_AWS_US_EAST_1,
			ServicePlan:    globalcommonv1.ServicePlan_Premium,
			OwnerType:      globalngv1.ResourcePool_ORGANIZATION,
			OwnerId:        30061,
			LifecycleState: globalngv1.ResourcePool_ACTIVE,
		}},
	}
	svc := &serviceImpl{
		clusterRepo:      &validateOnlyClusterRepo{},
		regionRepo:       &validateOnlyRegionRepo{},
		rcuQuotaRepo:     &validateOnlyRcuQuotaRepo{},
		billingCli:       &validateOnlyBillingClient{},
		encryptor:        &validateOnlyEncryptor{},
		resourcePoolSvc:  poolSvc,
		encryptionKeySvc: encryptionKeySvc,
	}

	cluster, err := svc.Create(ctx, &globalngv1.CreateClusterRequest{
		ValidateOnly: true,
		Cluster: &globalngv1.Cluster{
			ClusterId:       10007669285045832905,
			DisplayName:     "yj-cmek-test",
			OrganizationId:  30061,
			ProjectId:       3202083,
			CloudProviderId: globalcommonv1.CloudProvider_AWS,
			RegionId:        globalcommonv1.Region_AWS_US_EAST_1,
			ServicePlan:     globalcommonv1.ServicePlan_Premium,
			MaxRcu:          20000,
			DualLayerDataEncryption: &globalngv1.Cluster_DualLayerDataEncryption{
				Key: &globalngv1.Cluster_DualLayerDataEncryption_Cmek{
					Cmek: validateOnlyAwsCmek(),
				},
			},
		},
	})

	require.NotNil(t, err)
	assert.Nil(t, cluster)
	assert.Contains(t, err.Error(), "account not matched")
	assert.Equal(t, 1, encryptionKeySvc.validateCmekCalls)
}

type validateOnlyClusterRepo struct {
	clusterrepo.Repo
}

func (r *validateOnlyClusterRepo) Query(context.Context, *clusterrepo.Filter) ([]*entity.Cluster, bizerr.BizError) {
	return nil, nil
}

type validateOnlyRegionRepo struct {
	regionrepo.Repo
}

func (r *validateOnlyRegionRepo) SelectRegion(
	_ context.Context,
	filter regionrepo.Filter, //nolint:gocritic // hugeParam: must match interface signature
) (*entity.Region, bizerr.BizError) {
	return &entity.Region{
		Region: &globalcommonv1.Region{
			Id:              filter.RegionId,
			CloudProviderId: filter.CloudProvider,
			ServicePlan:     filter.ServicePlan,
			State:           globalcommonv1.Region_ACTIVE,
			IsPublic:        true,
		},
		RegionID:   101,
		ProviderID: 201,
	}, nil
}

type validateOnlyRcuQuotaRepo struct {
	rcuquotarepo.Repo
}

func (r *validateOnlyRcuQuotaRepo) Load(context.Context, uint64, string) (*entity.RcuQuota, bizerr.BizError) {
	return &entity.RcuQuota{
		InstanceMaxRcu:           20000,
		TotalMaxRcu:              100000,
		MinimalRcuBaseline:       0,
		MinimalRcuCoefficient:    0,
		ScaleDownCooldownMinutes: 720,
	}, nil
}

type validateOnlyBillingClient struct {
	billing.Client
}

func (c *validateOnlyBillingClient) BillingActionCheck(
	context.Context,
	billing.Action,
	uint64,
	*billing.Resource,
) (*billing.ActionCheckResult, bizerr.BizError) {
	return &billing.ActionCheckResult{Pass: true}, nil
}

type validateOnlyEncryptor struct {
	remoteencryptor.Client
}

func (e *validateOnlyEncryptor) EncryptBase64(_ context.Context, text string) (string, bizerr.BizError) {
	return "encrypted:" + text, nil
}

type validateOnlyEncryptionKeyService struct {
	encryptionkeysvc.Service

	defaultKeyCalls       int
	cmekCalls             int
	validateCmekCalls     int
	validatedCmek         *globalngv1.CustomerManagedEncryptionKey
	validatedSharedPoolID string
	validateCmekErr       bizerr.BizError
}

func (s *validateOnlyEncryptionKeyService) CreateDefaultKeyOrGrantPermission(
	context.Context,
	uint64,
	uint64,
	string,
) (*entity.EncryptionKey, bizerr.BizError) {
	s.defaultKeyCalls++
	return nil, bizerr.New(bizerr.ErrCodeParams, "providerIdentity is required")
}

func (s *validateOnlyEncryptionKeyService) CreateCustomerManagedEncryptionKey(
	context.Context,
	*globalngv1.CustomerManagedEncryptionKey,
	uint64,
	string,
	string,
) (*globalngv1.CustomerManagedEncryptionKey, bizerr.BizError) {
	s.cmekCalls++
	return nil, bizerr.New(bizerr.ErrCodeParams, "providerIdentity is required")
}

func (s *validateOnlyEncryptionKeyService) ValidateCustomerManagedEncryptionKey(
	_ context.Context,
	cmek *globalngv1.CustomerManagedEncryptionKey,
	sharedPoolID string,
) bizerr.BizError {
	s.validateCmekCalls++
	s.validatedCmek = cmek
	s.validatedSharedPoolID = sharedPoolID
	return s.validateCmekErr
}

func validateOnlyAwsCmek() *globalngv1.CustomerManagedEncryptionKey {
	return &globalngv1.CustomerManagedEncryptionKey{
		OrganizationId:  30061,
		CloudProviderId: globalcommonv1.CloudProvider_AWS,
		RegionId:        globalcommonv1.Region_AWS_US_EAST_1,
		ServicePlan:     globalcommonv1.ServicePlan_Premium,
		Key: &globalngv1.CustomerManagedEncryptionKey_AwsKms_{
			AwsKms: &globalngv1.CustomerManagedEncryptionKey_AwsKms{
				KmsKeyArn: "arn:aws:kms:us-east-1:123456789012:key/validate-only",
			},
		},
		Principal: &globalngv1.CustomerManagedEncryptionKey_AwsPrincipal{
			AwsPrincipal: &globalngv1.CmekAccessIamPrincipal_AwsCmekPrincipal{
				AccountId: "123456789012",
			},
		},
	}
}
