package br

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/mock/gomock"
	"google.golang.org/protobuf/types/known/timestamppb"

	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/entity"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/br"
	mockbrrepo "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/br/mock_br"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/infra/repoimpl/schema"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/pagination"
)

func TestListBackups(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	ctx := t.Context()

	t.Run("success - empty result", func(t *testing.T) {
		mockBackupRepo := mockbrrepo.NewMockBackupRepo(ctrl)
		mockBackupSettingRepo := mockbrrepo.NewMockBackupSettingRepo(ctrl)

		service := &serviceImpl{
			backupRepo:        mockBackupRepo,
			backupSettingRepo: mockBackupSettingRepo,
		}

		req := &globalngv1.ListBackupsRequest{
			ClusterIds: []string{"123"},
		}

		// Mock count returns 0
		mockBackupRepo.EXPECT().UnscopedCount(gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, filter *br.QueryBackupsFilter) (int64, bizerr.BizError) {
				assert.Equal(t, []uint64{123}, filter.ClusterIds)
				assert.Equal(t, []uint64{}, filter.OrganizationIds)
				assert.Equal(t, []uint64{}, filter.Ids)
				assert.False(t, *filter.Deleted)
				return int64(0), nil
			},
		)

		resp, err := service.ListBackups(ctx, req)
		require.NoError(t, err)
		assert.Equal(t, int32(0), resp.TotalSize)
		assert.Empty(t, resp.Backups)
		assert.Empty(t, resp.NextPageToken)
	})

	t.Run("exclude expired automatic backups", func(t *testing.T) {
		mockBackupRepo := mockbrrepo.NewMockBackupRepo(ctrl)
		service := &serviceImpl{backupRepo: mockBackupRepo}

		mockBackupRepo.EXPECT().UnscopedCount(gomock.Any(), gomock.Any()).DoAndReturn(
			func(_ context.Context, filter *br.QueryBackupsFilter) (int64, bizerr.BizError) {
				require.NotNil(t, filter.ExcludeExpiredAutoBackupsAt)
				return int64(0), nil
			},
		)

		_, err := service.ListBackups(ctx, &globalngv1.ListBackupsRequest{
			ClusterIds: []string{"123"},
		})
		require.NoError(t, err)
	})

	t.Run("success - with results", func(t *testing.T) {
		mockBackupRepo := mockbrrepo.NewMockBackupRepo(ctrl)
		mockBackupSettingRepo := mockbrrepo.NewMockBackupSettingRepo(ctrl)

		service := &serviceImpl{
			backupRepo:        mockBackupRepo,
			backupSettingRepo: mockBackupSettingRepo,
		}

		req := &globalngv1.ListBackupsRequest{
			ClusterIds: []string{"123"},
			PageSize:   10,
		}

		now := time.Now().UTC()
		backups := []*entity.Backup{
			{
				Backup: &schema.Backup{
					ID:              1,
					ClusterId:       123,
					TenantId:        456,
					State:           schema.BackupStateSuccess,
					BackupTime:      now,
					BackupSizeBytes: 1024,
					TriggerType:     schema.BackupTriggerTypeAuto,
					Type:            schema.BackupTypeSnapshot,
					CustomerManaged: true,
					EncryptionKeyId: 789,
				},
			},
		}

		// Mock count returns 1
		mockBackupRepo.EXPECT().UnscopedCount(gomock.Any(), gomock.Any()).Return(int64(1), nil)

		// Mock query returns backups
		mockBackupRepo.EXPECT().UnscopedQuery(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, filter *br.QueryBackupsFilter, page *pagination.Page) ([]*entity.Backup, bizerr.BizError) {
				assert.Equal(t, []uint64{123}, filter.ClusterIds)
				assert.Equal(t, int64(10), page.PageSize)
				return backups, nil
			},
		)

		resp, err := service.ListBackups(ctx, req)
		require.NoError(t, err)
		assert.Equal(t, int32(1), resp.TotalSize)
		assert.Len(t, resp.Backups, 1)
		assert.Equal(t, "1", resp.Backups[0].Id)
		assert.Equal(t, "123", resp.Backups[0].ClusterId)
		assert.Equal(t, "456", resp.Backups[0].OrganizationId)
		assert.Equal(t, globalngv1.Backup_STATE_SUCCESS, resp.Backups[0].State)
		assert.Equal(t, globalngv1.Backup_TRIGGER_TYPE_AUTO, resp.Backups[0].TriggerType)
		assert.Equal(t, globalngv1.Backup_TYPE_SNAPSHOT, resp.Backups[0].Type)
		assert.True(t, resp.Backups[0].CustomerManaged)
		assert.Equal(t, uint64(789), resp.Backups[0].EncryptionKeyId)
	})

	t.Run("error - validation fails - invalid cluster state", func(t *testing.T) {
		mockBackupRepo := mockbrrepo.NewMockBackupRepo(ctrl)
		mockBackupSettingRepo := mockbrrepo.NewMockBackupSettingRepo(ctrl)

		service := &serviceImpl{
			backupRepo:        mockBackupRepo,
			backupSettingRepo: mockBackupSettingRepo,
		}

		req := &globalngv1.ListBackupsRequest{
			OrganizationIds: []string{"456"},
			ClusterStates: []globalngv1.ListBackupsRequest_ClusterState{
				globalngv1.ListBackupsRequest_CLUSTER_STATE_UNSPECIFIED,
			},
		}

		resp, err := service.ListBackups(ctx, req)
		require.Error(t, err)
		assert.Nil(t, resp)
		assert.Contains(t, err.Error(), "cluster state should be set to one of ACTIVE or DELETED")
	})

	t.Run("error - count fails", func(t *testing.T) {
		mockBackupRepo := mockbrrepo.NewMockBackupRepo(ctrl)
		mockBackupSettingRepo := mockbrrepo.NewMockBackupSettingRepo(ctrl)

		service := &serviceImpl{
			backupRepo:        mockBackupRepo,
			backupSettingRepo: mockBackupSettingRepo,
		}

		req := &globalngv1.ListBackupsRequest{
			ClusterIds: []string{"123"},
		}

		expectedErr := bizerr.New(bizerr.ErrCodeDB, "count error")
		mockBackupRepo.EXPECT().UnscopedCount(gomock.Any(), gomock.Any()).Return(int64(0), expectedErr)

		resp, err := service.ListBackups(ctx, req)
		require.Error(t, err)
		assert.Nil(t, resp)
		assert.Contains(t, err.Error(), "CountBackups")
	})

	t.Run("error - query fails", func(t *testing.T) {
		mockBackupRepo := mockbrrepo.NewMockBackupRepo(ctrl)
		mockBackupSettingRepo := mockbrrepo.NewMockBackupSettingRepo(ctrl)

		service := &serviceImpl{
			backupRepo:        mockBackupRepo,
			backupSettingRepo: mockBackupSettingRepo,
		}

		req := &globalngv1.ListBackupsRequest{
			ClusterIds: []string{"123"},
			PageSize:   10,
		}

		// Mock count succeeds
		mockBackupRepo.EXPECT().UnscopedCount(gomock.Any(), gomock.Any()).Return(int64(1), nil)

		// Mock query fails
		expectedErr := bizerr.New(bizerr.ErrCodeDB, "query error")
		mockBackupRepo.EXPECT().UnscopedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, expectedErr)

		resp, err := service.ListBackups(ctx, req)
		require.Error(t, err)
		assert.Nil(t, resp)
		assert.Contains(t, err.Error(), "RemotePagination")
	})
}

func TestValidateListBackupsRequest(t *testing.T) {
	t.Run("valid request with cluster ids", func(t *testing.T) {
		req := &globalngv1.ListBackupsRequest{
			ClusterIds: []string{"123"},
		}

		err := validateListBackupsRequest(req)
		assert.NoError(t, err)
	})

	t.Run("valid request with organization ids", func(t *testing.T) {
		req := &globalngv1.ListBackupsRequest{
			OrganizationIds: []string{"456"},
		}

		err := validateListBackupsRequest(req)
		assert.NoError(t, err)
	})

	t.Run("valid request with backup ids", func(t *testing.T) {
		req := &globalngv1.ListBackupsRequest{
			BackupIds: []string{"789"},
		}

		err := validateListBackupsRequest(req)
		assert.NoError(t, err)
	})

	t.Run("valid request with cluster states", func(t *testing.T) {
		req := &globalngv1.ListBackupsRequest{
			OrganizationIds: []string{"456"},
			ClusterStates: []globalngv1.ListBackupsRequest_ClusterState{
				globalngv1.ListBackupsRequest_ACTIVE,
			},
		}

		err := validateListBackupsRequest(req)
		assert.NoError(t, err)
	})

	t.Run("invalid request - no filter criteria", func(t *testing.T) {
		req := &globalngv1.ListBackupsRequest{}

		err := validateListBackupsRequest(req)
		require.Error(t, err)
		assert.Contains(t, err.Error(), "at least one of cluster ids, organization ids or backup ids must be set")
	})

	t.Run("invalid request - multiple cluster states", func(t *testing.T) {
		req := &globalngv1.ListBackupsRequest{
			OrganizationIds: []string{"456"},
			ClusterStates: []globalngv1.ListBackupsRequest_ClusterState{
				globalngv1.ListBackupsRequest_ACTIVE,
				globalngv1.ListBackupsRequest_DELETED,
			},
		}

		err := validateListBackupsRequest(req)
		require.Error(t, err)
		assert.Contains(t, err.Error(), "cluster states should only have one element")
	})

	t.Run("invalid request - invalid cluster state", func(t *testing.T) {
		req := &globalngv1.ListBackupsRequest{
			OrganizationIds: []string{"456"},
			ClusterStates: []globalngv1.ListBackupsRequest_ClusterState{
				globalngv1.ListBackupsRequest_CLUSTER_STATE_UNSPECIFIED,
			},
		}

		err := validateListBackupsRequest(req)
		require.Error(t, err)
		assert.Contains(t, err.Error(), "cluster state should be set to one of ACTIVE or DELETED")
	})

	t.Run("invalid request - cluster states without organization ids", func(t *testing.T) {
		req := &globalngv1.ListBackupsRequest{
			ClusterStates: []globalngv1.ListBackupsRequest_ClusterState{globalngv1.ListBackupsRequest_ACTIVE},
			BackupIds:     []string{"789"},
		}

		err := validateListBackupsRequest(req)
		require.Error(t, err)
		assert.Contains(t, err.Error(), "organization ids must be set when cluster states are set")
	})

	t.Run("invalid request - cluster ids and cluster states conflict", func(t *testing.T) {
		req := &globalngv1.ListBackupsRequest{
			ClusterIds:      []string{"123"},
			OrganizationIds: []string{"456"},
			ClusterStates: []globalngv1.ListBackupsRequest_ClusterState{
				globalngv1.ListBackupsRequest_ACTIVE,
			},
		}

		err := validateListBackupsRequest(req)
		require.Error(t, err)
		assert.Contains(t, err.Error(), "cluster ids and cluster states cannot be set at the same time")
	})

	t.Run("invalid request - backup ids and cluster states conflict", func(t *testing.T) {
		req := &globalngv1.ListBackupsRequest{
			BackupIds:       []string{"789"},
			OrganizationIds: []string{"456"},
			ClusterStates: []globalngv1.ListBackupsRequest_ClusterState{
				globalngv1.ListBackupsRequest_ACTIVE,
			},
		}

		err := validateListBackupsRequest(req)
		require.Error(t, err)
		assert.Contains(t, err.Error(), "backup ids and cluster states cannot be set at the same time")
	})

	t.Run("invalid request - start time after end time", func(t *testing.T) {
		req := &globalngv1.ListBackupsRequest{
			ClusterIds: []string{"123"},
			StartTime:  timestamppb.New(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)),
			EndTime:    timestamppb.New(time.Date(2023, 12, 31, 23, 59, 59, 0, time.UTC)),
		}

		err := validateListBackupsRequest(req)
		require.Error(t, err)
		assert.Contains(t, err.Error(), "start time must be before or equal to end time")
	})
}

func TestGetBackupFilter(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	ctx := t.Context()

	t.Run("success - with cluster ids", func(t *testing.T) {
		mockBackupSettingRepo := mockbrrepo.NewMockBackupSettingRepo(ctrl)

		service := &serviceImpl{
			backupSettingRepo: mockBackupSettingRepo,
		}

		req := &globalngv1.ListBackupsRequest{
			ClusterIds:      []string{"123", "456"},
			OrganizationIds: []string{"789"},
			BackupIds:       []string{"1", "2"},
			States:          []globalngv1.Backup_State{globalngv1.Backup_STATE_SUCCESS},
			ServicePlans:    []globalcommonv1.ServicePlan{globalcommonv1.ServicePlan_Premium},
			RegionIds:       []string{"us-west-2"},
			TriggerTypes:    []globalngv1.Backup_TriggerType{globalngv1.Backup_TRIGGER_TYPE_AUTO},
			StartTime:       timestamppb.New(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)),
			EndTime:         timestamppb.New(time.Date(2024, 12, 31, 23, 59, 59, 0, time.UTC)),
		}

		filter, err := service.getBackupFilter(ctx, req)
		require.NoError(t, err)
		assert.Equal(t, []uint64{1, 2}, filter.Ids)
		assert.Equal(t, []uint64{123, 456}, filter.ClusterIds)
		assert.Equal(t, []uint64{789}, filter.OrganizationIds)
		assert.Equal(t, []schema.BackupState{schema.BackupStateSuccess}, filter.States)
		assert.Equal(t, []string{"Premium"}, filter.ServicePlans)
		assert.Equal(t, []string{"us-west-2"}, filter.RegionIds)
		assert.Equal(t, []schema.BackupTriggerType{schema.BackupTriggerTypeAuto}, filter.TriggerTypes)
		assert.NotNil(t, filter.BackupTimeGte)
		assert.NotNil(t, filter.BackupTimeLte)
		assert.False(t, *filter.Deleted)
	})

	t.Run("error - invalid cluster ids", func(t *testing.T) {
		mockBackupSettingRepo := mockbrrepo.NewMockBackupSettingRepo(ctrl)

		service := &serviceImpl{
			backupSettingRepo: mockBackupSettingRepo,
		}

		req := &globalngv1.ListBackupsRequest{
			ClusterIds: []string{"invalid"},
		}

		filter, err := service.getBackupFilter(ctx, req)
		require.Error(t, err)
		assert.Nil(t, filter)
		assert.Contains(t, err.Error(), "invalid cluster ids")
	})

	t.Run("error - invalid organization ids", func(t *testing.T) {
		mockBackupSettingRepo := mockbrrepo.NewMockBackupSettingRepo(ctrl)

		service := &serviceImpl{
			backupSettingRepo: mockBackupSettingRepo,
		}

		req := &globalngv1.ListBackupsRequest{
			OrganizationIds: []string{"invalid"},
		}

		filter, err := service.getBackupFilter(ctx, req)
		require.Error(t, err)
		assert.Nil(t, filter)
		assert.Contains(t, err.Error(), "invalid organization ids")
	})

	t.Run("error - invalid backup ids", func(t *testing.T) {
		mockBackupSettingRepo := mockbrrepo.NewMockBackupSettingRepo(ctrl)

		service := &serviceImpl{
			backupSettingRepo: mockBackupSettingRepo,
		}

		req := &globalngv1.ListBackupsRequest{
			BackupIds: []string{"invalid"},
		}

		filter, err := service.getBackupFilter(ctx, req)
		require.Error(t, err)
		assert.Nil(t, filter)
		assert.Contains(t, err.Error(), "invalid backup ids")
	})
}

func TestGetClusterIds(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	ctx := t.Context()

	t.Run("success - user set cluster ids", func(t *testing.T) {
		mockBackupSettingRepo := mockbrrepo.NewMockBackupSettingRepo(ctrl)

		service := &serviceImpl{
			backupSettingRepo: mockBackupSettingRepo,
		}

		req := &globalngv1.ListBackupsRequest{}
		userSetClusterIds := []uint64{123, 456}
		userSetOrganizationIds := []uint64{789}

		clusterIds, err := service.getClusterIds(ctx, req, userSetClusterIds, userSetOrganizationIds)
		require.NoError(t, err)
		assert.Equal(t, []uint64{123, 456}, clusterIds)
	})

	t.Run("success - with cluster states", func(t *testing.T) {
		mockBackupSettingRepo := mockbrrepo.NewMockBackupSettingRepo(ctrl)

		service := &serviceImpl{
			backupSettingRepo: mockBackupSettingRepo,
		}

		req := &globalngv1.ListBackupsRequest{
			ClusterStates: []globalngv1.ListBackupsRequest_ClusterState{globalngv1.ListBackupsRequest_ACTIVE},
		}
		userSetClusterIds := []uint64{}
		userSetOrganizationIds := []uint64{456}

		backupSettings := []*entity.BackupSetting{
			{
				BackupSetting: &schema.BackupSetting{
					ClusterId: 123,
				},
			},
			{
				BackupSetting: &schema.BackupSetting{
					ClusterId: 789,
				},
			},
		}

		mockBackupSettingRepo.EXPECT().UnscopedQuery(ctx, gomock.Any(), nil).DoAndReturn(
			func(ctx context.Context, filter *br.QueryBackupSettingFilter, page *pagination.Page) ([]*entity.BackupSetting, bizerr.BizError) {
				assert.Equal(t, []uint64{456}, filter.TenantIds)
				assert.Equal(t, []schema.BackupSettingState{schema.BackupSettingStateCreated}, filter.States)
				return backupSettings, nil
			},
		)

		clusterIds, err := service.getClusterIds(ctx, req, userSetClusterIds, userSetOrganizationIds)
		require.NoError(t, err)
		assert.Equal(t, []uint64{123, 789}, clusterIds)
	})

	t.Run("success - no cluster states", func(t *testing.T) {
		mockBackupSettingRepo := mockbrrepo.NewMockBackupSettingRepo(ctrl)

		service := &serviceImpl{
			backupSettingRepo: mockBackupSettingRepo,
		}

		req := &globalngv1.ListBackupsRequest{}
		userSetClusterIds := []uint64{}
		userSetOrganizationIds := []uint64{456}

		clusterIds, err := service.getClusterIds(ctx, req, userSetClusterIds, userSetOrganizationIds)
		require.NoError(t, err)
		assert.Nil(t, clusterIds)
	})

	t.Run("error - backup setting query fails", func(t *testing.T) {
		mockBackupSettingRepo := mockbrrepo.NewMockBackupSettingRepo(ctrl)

		service := &serviceImpl{
			backupSettingRepo: mockBackupSettingRepo,
		}

		req := &globalngv1.ListBackupsRequest{
			ClusterStates: []globalngv1.ListBackupsRequest_ClusterState{globalngv1.ListBackupsRequest_ACTIVE},
		}
		userSetClusterIds := []uint64{}
		userSetOrganizationIds := []uint64{456}

		expectedErr := bizerr.New(bizerr.ErrCodeDB, "database error")
		mockBackupSettingRepo.EXPECT().UnscopedQuery(ctx, gomock.Any(), nil).Return(nil, expectedErr)

		clusterIds, err := service.getClusterIds(ctx, req, userSetClusterIds, userSetOrganizationIds)
		require.Error(t, err)
		assert.Nil(t, clusterIds)
		assert.Contains(t, err.Error(), "GetClusterIdsByStates")
	})
}

func TestGetTimePtr(t *testing.T) {
	t.Run("nil timestamp", func(t *testing.T) {
		result := getTimePtr(nil)
		assert.Nil(t, result)
	})

	t.Run("valid timestamp", func(t *testing.T) {
		expectedTime := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
		protoTime := timestamppb.New(expectedTime)

		result := getTimePtr(protoTime)
		require.NotNil(t, result)
		assert.Equal(t, expectedTime, *result)
	})
}
