package dedicatedtopremiumprecheck

import (
	"context"
	"errors"
	"testing"

	"github.com/stretchr/testify/require"
	accountsdk "github.com/tidbcloud/account/idl/pbgen/proto/account"
	"go.uber.org/mock/gomock"

	globaldedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/adapter/account/mock_account"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/adapter/dedicatedglobal/mock_dedicatedglobal"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/repository"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/repository/mock_repository"
)

func TestListFiltersAndPaginates(t *testing.T) {
	ctrl := gomock.NewController(t)
	precheckRepo := mock_repository.NewMockDedicatedToPremiumPrecheckRepo(ctrl)
	precheckRepo.EXPECT().Count(gomock.Any(), &repository.DedicatedToPremiumPrecheckFilter{
		SourceClusterID: 42,
		Limit:           2,
		Offset:          2,
	}).Return(uint32(5), nil)
	precheckRepo.EXPECT().List(gomock.Any(), &repository.DedicatedToPremiumPrecheckFilter{
		SourceClusterID: 42,
		Limit:           2,
		Offset:          2,
	}).Return([]*entity.DedicatedToPremiumPrecheck{{ID: 3}, {ID: 2}}, nil)

	prechecks, total, err := (&service{precheckRepo: precheckRepo}).List(
		context.Background(),
		42,
		2,
		2,
	)
	require.NoError(t, err)
	require.Equal(t, uint32(5), total)
	require.Equal(t, []uint64{3, 2}, []uint64{prechecks[0].ID, prechecks[1].ID})
}

func TestListRejectsOversizedPage(t *testing.T) {
	_, _, err := (&service{}).List(context.Background(), 0, maxListPageSize+1, 1)
	require.Error(t, err)
}

func TestNormalizePageSizeUsesDefault(t *testing.T) {
	pageSize, err := normalizePageSize(0)
	require.NoError(t, err)
	require.Equal(t, 10, pageSize)
}

func TestCreateRejectsInvalidApplicant(t *testing.T) {
	_, err := (&service{}).Create(
		context.Background(),
		42,
		"migration@pingcap.com",
		nil,
		"not-an-email",
	)
	require.ErrorContains(t, err, "applicant is invalid")
}

func TestCreateAllowsMissingMigrationAccount(t *testing.T) {
	clusterRepo := &createTestClusterRepository{cluster: &entity.Cluster{
		ClusterId:  42,
		DeployType: entity.DeployTypeServerless,
	}}

	_, err := (&service{clusterRepo: clusterRepo}).Create(
		context.Background(),
		42,
		"",
		nil,
		"applicant@pingcap.com",
	)

	require.ErrorContains(t, err, "source cluster must be an active Dedicated cluster")
}

func TestCreateRejectsMigrationAccountOutsideSourceOrganization(t *testing.T) {
	const (
		clusterID = uint64(42)
		orgID     = uint64(100)
		email     = "migration@pingcap.com"
	)
	ctrl := gomock.NewController(t)
	accountCli := mock_account.NewMockClient(ctrl)
	clusterRepo := &createTestClusterRepository{cluster: &entity.Cluster{
		ClusterId:      clusterID,
		OrganizationId: orgID,
		DeployType:     entity.DeployTypeDedicated,
	}}
	accountCli.EXPECT().IsUserInOrg(gomock.Any(), email, orgID).Return(false, nil)

	_, err := (&service{clusterRepo: clusterRepo, accountCli: accountCli}).Create(
		context.Background(),
		clusterID,
		email,
		nil,
		"applicant@pingcap.com",
	)
	require.ErrorContains(t, err, "migration account migration@pingcap.com does not belong to source cluster organization 100")
}

func TestCreateRejectsNonAvailableDedicatedClusterState(t *testing.T) {
	const clusterID = uint64(42)
	ctrl := gomock.NewController(t)
	dedicatedCli := mock_dedicatedglobal.NewMockDedicatedGlobal(ctrl)
	clusterRepo := &createTestClusterRepository{cluster: &entity.Cluster{
		ClusterId:  clusterID,
		DeployType: entity.DeployTypeDedicated,
	}}
	dedicatedCli.EXPECT().GetCluster(gomock.Any(), &globaldedicatedv1.GetClusterRequest{
		ClusterId: clusterID,
	}).Return(&globaldedicatedv1.Cluster{State: globaldedicatedv1.Cluster_CREATING}, nil)

	_, err := (&service{clusterRepo: clusterRepo, dedicatedCli: dedicatedCli}).Create(
		context.Background(),
		clusterID,
		"",
		nil,
		"applicant@pingcap.com",
	)

	require.ErrorContains(t, err, "source Dedicated cluster must be AVAILABLE, current state is CREATING")
}

type createTestClusterRepository struct {
	repository.ClusterRepository

	cluster *entity.Cluster
}

func (r *createTestClusterRepository) GetCluster(
	context.Context,
	uint64,
	bool,
) (*entity.Cluster, error) {
	return r.cluster, nil
}

func TestValidateTargetProject(t *testing.T) {
	const (
		projectID = uint64(99)
		orgID     = uint64(100)
	)
	tests := []struct {
		name       string
		project    *accountsdk.Project
		accountErr error
		errMessage string
	}{
		{
			name:    "TiDB X project in source organization",
			project: &accountsdk.Project{Id: projectID, TenantId: orgID, Type: accountsdk.ProjectType_PROJECT_TYPE_TIDBX},
		},
		{
			name:       "Dedicated project",
			project:    &accountsdk.Project{Id: projectID, TenantId: orgID, Type: accountsdk.ProjectType_PROJECT_TYPE_DEDICATED},
			errMessage: "target project 99 must be a TiDB X project",
		},
		{
			name:       "TiDB X project in another organization",
			project:    &accountsdk.Project{Id: projectID, TenantId: orgID + 1, Type: accountsdk.ProjectType_PROJECT_TYPE_TIDBX},
			errMessage: "target project 99 does not belong to source cluster organization 100",
		},
		{
			name:       "account service failure",
			accountErr: errors.New("account unavailable"),
			errMessage: "failed to get target project",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctrl := gomock.NewController(t)
			accountCli := mock_account.NewMockClient(ctrl)
			accountCli.EXPECT().GetProject(gomock.Any(), projectID).Return(tt.project, tt.accountErr)

			err := (&service{accountCli: accountCli}).validateTargetProject(context.Background(), projectID, orgID)
			if tt.errMessage == "" {
				require.NoError(t, err)
				return
			}
			require.ErrorContains(t, err, tt.errMessage)
		})
	}
}
