package server

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	clusteragentv1 "github.com/tidbcloud/cluster-service-ng/api/tidbcloud/cluster-agent/v1"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	clustergatewaymocks "github.com/tidbcloud/cluster-service-ng/pkg/clustergateway/mocks"
	repomocks "github.com/tidbcloud/cluster-service-ng/pkg/storage/repo/mocks"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/timestamppb"
)

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

	clusterRepo, sharedPoolRepo, gateway, svc, sharedPool := newPlanReplayerServer(t)
	clusterRepo.On("Load", mock.Anything, "123", false).Return(
		&entity.TiDBCluster{Id: "123", SharedPoolId: sharedPool.GetId()},
		nil,
	)
	sharedPoolRepo.On("Load", mock.Anything, sharedPool.GetId(), false).Return(sharedPool, nil)
	lastModified := timestamppb.New(time.Date(2026, 8, 10, 1, 2, 3, 0, time.UTC))
	gateway.On(
		"ListPlanReplayerFiles",
		mock.Anything,
		sharedPool,
		mock.MatchedBy(func(req *clusteragentv1.ListPlanReplayerFilesReq) bool {
			return req.GetClusterId() == "123" &&
				req.GetStorageRoot() == "s3://physical-internal/prefix/global-sort" &&
				req.GetPageSize() == defaultPlanReplayerPageSize
		}),
	).Return(&clusteragentv1.ListPlanReplayerFilesResp{
		Files: []*clusteragentv1.PlanReplayerFile{{
			FileName:     "replayer_token_1.zip",
			SizeBytes:    42,
			LastModified: lastModified,
			StorageUri:   "s3://physical-internal/prefix/global-sort/123/replayer/replayer_token_1.zip",
		}},
		NextPageToken: "next",
	}, nil)

	resp, err := svc.ListPlanReplayerFiles(context.Background(), &apiv1.ListPlanReplayerFilesRequest{
		ClusterId: "123",
	})
	require.NoError(t, err)
	require.Len(t, resp.GetFiles(), 1)
	assert.Equal(t, "replayer_token_1.zip", resp.GetFiles()[0].GetFileName())
	assert.Equal(t, int64(42), resp.GetFiles()[0].GetSizeBytes())
	assert.Equal(t, "next", resp.GetNextPageToken())
}

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

	clusterRepo, sharedPoolRepo, gateway, svc, sharedPool := newPlanReplayerServer(t)
	clusterRepo.On("Load", mock.Anything, "123", false).Return(
		&entity.TiDBCluster{Id: "123", SharedPoolId: sharedPool.GetId()},
		nil,
	)
	sharedPoolRepo.On("Load", mock.Anything, sharedPool.GetId(), false).Return(sharedPool, nil)
	expiresAt := timestamppb.New(time.Date(2026, 8, 10, 2, 2, 3, 0, time.UTC))
	gateway.On(
		"GeneratePlanReplayerFileDownloadURL",
		mock.Anything,
		sharedPool,
		mock.MatchedBy(func(req *clusteragentv1.GeneratePlanReplayerFileDownloadURLReq) bool {
			return req.GetClusterId() == "123" &&
				req.GetStorageRoot() == "s3://physical-internal/prefix/global-sort" &&
				req.GetFileName() == "replayer_token_1.zip"
		}),
	).Return(&clusteragentv1.GeneratePlanReplayerFileDownloadURLResp{
		DownloadUrl: "https://download.example/file",
		ExpireTime:  expiresAt,
	}, nil)

	resp, err := svc.GeneratePlanReplayerFileDownloadURL(
		context.Background(),
		&apiv1.GeneratePlanReplayerFileDownloadURLRequest{
			ClusterId: "123",
			FileName:  "replayer_token_1.zip",
		},
	)
	require.NoError(t, err)
	assert.Equal(t, "https://download.example/file", resp.GetDownloadUrl())
	assert.Equal(t, expiresAt, resp.GetExpireTime())
}

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

	_, _, _, svc, _ := newPlanReplayerServer(t)
	_, err := svc.ListPlanReplayerFiles(context.Background(), &apiv1.ListPlanReplayerFilesRequest{
		ClusterId: "not-numeric",
	})
	require.Error(t, err)
	assert.Equal(t, codes.InvalidArgument, status.Code(err))

	_, err = svc.GeneratePlanReplayerFileDownloadURL(
		context.Background(),
		&apiv1.GeneratePlanReplayerFileDownloadURLRequest{
			ClusterId: "123",
			FileName:  "../replayer_token_1.zip",
		},
	)
	require.Error(t, err)
	assert.Equal(t, codes.InvalidArgument, status.Code(err))
}

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

	clusterRepo, sharedPoolRepo, _, svc, sharedPool := newPlanReplayerServer(t)
	sharedPool.InternalStorage = &apiv1.SharedPool_ObjectStorage{}
	sharedPool.ObjectStorage = &apiv1.SharedPool_ObjectStorage{}
	clusterRepo.On("Load", mock.Anything, "123", false).Return(
		&entity.TiDBCluster{Id: "123", SharedPoolId: sharedPool.GetId()},
		nil,
	)
	sharedPoolRepo.On("Load", mock.Anything, sharedPool.GetId(), false).Return(sharedPool, nil)

	_, err := svc.ListPlanReplayerFiles(context.Background(), &apiv1.ListPlanReplayerFilesRequest{
		ClusterId: "123",
	})
	require.Error(t, err)
	assert.Equal(t, codes.FailedPrecondition, status.Code(err))
}

func newPlanReplayerServer(
	t *testing.T,
) (
	*repomocks.TiDBClusterRepo,
	*repomocks.SharedPoolRepo,
	*clustergatewaymocks.Client,
	*serverImpl,
	*entity.SharedPool,
) {
	t.Helper()

	clusterRepo := repomocks.NewTiDBClusterRepo(t)
	sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
	gateway := clustergatewaymocks.NewClient(t)
	sharedPool := &entity.SharedPool{SharedPool: apiv1.SharedPool{
		Id:       "pool-1",
		Provider: &apiv1.Provider{Type: apiv1.ProviderType_AWS},
		ObjectStorage: &apiv1.SharedPool_ObjectStorage{
			Bucket: "physical-data",
		},
		InternalStorage: &apiv1.SharedPool_ObjectStorage{
			Bucket: "physical-internal",
			Prefix: "prefix",
		},
	}}
	return clusterRepo, sharedPoolRepo, gateway, &serverImpl{
		tidbClusterRepo:   clusterRepo,
		sharedPoolRepo:    sharedPoolRepo,
		clustergatewayCli: gateway,
	}, sharedPool
}
