package service

import (
	"context"
	"errors"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	apiv1 "github.com/tidbcloud/cluster-service-ng/api/tidbcloud/cluster-agent/v1"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

	"github.com/tidbcloud/cluster-service-ng/internal/cluster-agent/internal/objectstore"
)

type stubPlanReplayerStore struct {
	listResult objectstore.ListResult
	listErr    error
	listCalls  int
	pageSize   int32
	cursor     string

	headErr       error
	headCalls     int
	presignResult objectstore.SignedDownload
	presignErr    error
	presignCalls  int
	objectKey     string
	expires       time.Duration
}

func (s *stubPlanReplayerStore) List(
	ctx context.Context,
	location objectstore.Location,
	prefix string,
	pageSize int32,
	continuationToken string,
) (objectstore.ListResult, error) {
	_ = ctx
	_ = location
	_ = prefix
	s.listCalls++
	s.pageSize = pageSize
	s.cursor = continuationToken
	return s.listResult, s.listErr
}

func (s *stubPlanReplayerStore) Head(
	ctx context.Context,
	location objectstore.Location,
	objectKey string,
) error {
	_ = ctx
	_ = location
	s.headCalls++
	s.objectKey = objectKey
	return s.headErr
}

func (s *stubPlanReplayerStore) PresignGet(
	ctx context.Context,
	location objectstore.Location,
	objectKey string,
	expires time.Duration,
) (objectstore.SignedDownload, error) {
	_ = ctx
	_ = location
	s.presignCalls++
	s.objectKey = objectKey
	s.expires = expires
	return s.presignResult, s.presignErr
}

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

	lastModified := time.Date(2026, 8, 10, 1, 2, 3, 0, time.UTC)
	store := &stubPlanReplayerStore{listResult: objectstore.ListResult{
		Objects: []objectstore.ObjectMetadata{
			{
				Key:          "prefix/global-sort/123/replayer/replayer_token_1.zip",
				SizeBytes:    42,
				LastModified: lastModified,
			},
			{
				Key:          "prefix/global-sort/123/replayer/replayer_token_1.txt",
				LastModified: lastModified,
			},
		},
		IsTruncated:           true,
		NextContinuationToken: "native-next",
	}}
	svc := &AgentService{
		provider:          objectstore.ProviderAWS,
		planReplayerStore: store,
	}

	resp, err := svc.ListPlanReplayerFiles(context.Background(), &apiv1.ListPlanReplayerFilesReq{
		ClusterId:   "123",
		StorageRoot: "s3://bucket/prefix/global-sort",
	})
	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, "s3://bucket/prefix/global-sort/123/replayer/replayer_token_1.zip", resp.GetFiles()[0].GetStorageUri())
	assert.Equal(t, lastModified, resp.GetFiles()[0].GetLastModified().AsTime())
	assert.Equal(t, int32(objectstore.DefaultPageSize), store.pageSize)
	assert.Equal(t, 1, store.listCalls)

	cursor, err := objectstore.DecodePageToken(
		resp.GetNextPageToken(),
		"s3://bucket/prefix/global-sort/123/replayer/replayer_",
	)
	require.NoError(t, err)
	assert.Equal(t, "native-next", cursor)
}

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

	store := &stubPlanReplayerStore{listResult: objectstore.ListResult{
		Objects: []objectstore.ObjectMetadata{{
			Key:          "prefix/global-sort/123/replayer/replayer_token_1.txt",
			LastModified: time.Now(),
		}},
		IsTruncated:           true,
		NextContinuationToken: "native-next",
	}}
	svc := &AgentService{
		provider:          objectstore.ProviderAWS,
		planReplayerStore: store,
	}
	resp, err := svc.ListPlanReplayerFiles(context.Background(), &apiv1.ListPlanReplayerFilesReq{
		ClusterId:   "123",
		StorageRoot: "s3://bucket/prefix/global-sort",
	})
	require.NoError(t, err)
	assert.Empty(t, resp.GetFiles())
	assert.NotEmpty(t, resp.GetNextPageToken())
	assert.Equal(t, 1, store.listCalls)
}

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

	expiresAt := time.Now().UTC().Add(time.Hour)
	store := &stubPlanReplayerStore{presignResult: objectstore.SignedDownload{
		URL:       "https://download.example/file",
		ExpiresAt: expiresAt,
	}}
	svc := &AgentService{
		provider:          objectstore.ProviderAWS,
		planReplayerStore: store,
	}
	resp, err := svc.GeneratePlanReplayerFileDownloadURL(
		context.Background(),
		&apiv1.GeneratePlanReplayerFileDownloadURLReq{
			ClusterId:   "123",
			StorageRoot: "s3://bucket/prefix/global-sort",
			FileName:    "replayer_token_1.zip",
		},
	)
	require.NoError(t, err)
	assert.Equal(t, "https://download.example/file", resp.GetDownloadUrl())
	assert.Equal(t, expiresAt, resp.GetExpireTime().AsTime())
	assert.Equal(t, 1, store.headCalls)
	assert.Equal(t, 1, store.presignCalls)
	assert.Equal(t, "prefix/global-sort/123/replayer/replayer_token_1.zip", store.objectKey)
	assert.Equal(t, time.Hour, store.expires)
}

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

	store := &stubPlanReplayerStore{headErr: objectstore.ErrObjectNotFound}
	svc := &AgentService{
		provider:          objectstore.ProviderAWS,
		planReplayerStore: store,
	}
	_, err := svc.GeneratePlanReplayerFileDownloadURL(
		context.Background(),
		&apiv1.GeneratePlanReplayerFileDownloadURLReq{
			ClusterId:   "123",
			StorageRoot: "s3://bucket/global-sort",
			FileName:    "replayer_token_1.zip",
		},
	)
	require.Error(t, err)
	assert.Equal(t, codes.NotFound, status.Code(err))
	assert.Equal(t, 0, store.presignCalls)
}

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

	tests := []struct {
		err  error
		code codes.Code
	}{
		{err: objectstore.ErrInvalidCursor, code: codes.InvalidArgument},
		{err: objectstore.ErrAccessDenied, code: codes.PermissionDenied},
		{err: objectstore.ErrUnavailable, code: codes.Unavailable},
		{err: errors.New("other"), code: codes.Internal},
	}
	for _, tt := range tests {
		assert.Equal(t, tt.code, status.Code(planReplayerStoreStatus("operation", tt.err)))
	}
}
