//nolint:testpackage,funlen
package impl

import (
	"context"
	"testing"
	"time"

	"github.com/AlekSi/pointer"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/suite"
	"github.com/tidbcloud/core-svc/idl/pbgen/proto/core"
	cluster_job_mock "github.com/tidbcloud/dataflow-service/app/internal/cluster_job/mock"
	feature_gate_mock "github.com/tidbcloud/dataflow-service/app/internal/feature_gate/mock"
	backup_mock "github.com/tidbcloud/dataflow-service/app/models/backup/mock"
	"github.com/tidbcloud/dataflow-service/app/models/domain"
	"github.com/tidbcloud/dataflow-service/app/repo"
	backup_repo "github.com/tidbcloud/dataflow-service/app/repo/backup"
	"github.com/tidbcloud/dataflow-service/app/repo/entity"
	repo_impl "github.com/tidbcloud/dataflow-service/app/repo/impl"
	restore_repo "github.com/tidbcloud/dataflow-service/app/repo/restore"
	workflow_mock "github.com/tidbcloud/dataflow-service/app/workflow/mock"
	dataflow "github.com/tidbcloud/dataflow-service/idl/pbgen/proto/dataflow"
	grpcli_mock "github.com/tidbcloud/dataflow-service/pkg/grpc/grpcli/mock"
	"github.com/tidbcloud/pkgv2/db"
	dedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	mgmt "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

type snapshotBackupModelTestSuit struct {
	suite.Suite
	db    *gorm.DB
	model *SnapshotBackupModel

	bucketRepo           repo.BucketRepo
	backupRepo           backup_repo.BackupRepo
	scheduleRepo         backup_repo.BackupScheduleRepo
	restoreRepo          restore_repo.RestoreRepo
	mockBackupExportRepo backup_repo.BackupExportRepo

	mockJob                *cluster_job_mock.Model
	mockCoreCli            *grpcli_mock.CoreGrpcClient
	mockImportCli          *grpcli_mock.ImportGrpcClient
	mockAccountCli         *grpcli_mock.AccountSvcGrpcClient
	mockWorkflowModel      *workflow_mock.Workflow
	mockBackupSettingModel *backup_mock.BackupSettingModel
}

func (s *snapshotBackupModelTestSuit) SetupSuite() {
	// Backup.BeforeSave calls GetEventHookerManager() which panics if nil.
	entity.NewDataflowEventHookerManager()

	testDB, err := repo.GetFakeDB()
	s.NoError(err)
	s.db = testDB

	dbProvider := db.NewProvider(testDB)
	s.bucketRepo = repo_impl.NewBucketRepo(dbProvider)
	s.backupRepo = backup_repo.NewBackupRepo(dbProvider)
	s.scheduleRepo = backup_repo.NewBackupScheduleRepo(dbProvider)
	s.restoreRepo = restore_repo.NewRestoreRepo(dbProvider)
	s.mockBackupExportRepo = backup_repo.NewBackupExportRepo(dbProvider)

	s.mockJob = cluster_job_mock.NewModel(s.T())
	s.mockCoreCli = grpcli_mock.NewCoreGrpcClient(s.T())
	mockMgmtCli := grpcli_mock.NewMgmtSvcGrpcClient(s.T())
	s.mockImportCli = grpcli_mock.NewImportGrpcClient(s.T())
	s.mockAccountCli = grpcli_mock.NewAccountSvcGrpcClient(s.T())
	s.mockWorkflowModel = workflow_mock.NewWorkflow(s.T())
	s.mockBackupSettingModel = backup_mock.NewBackupSettingModel(s.T())
	mockFeatureGate := feature_gate_mock.NewFeatureGate(s.T())

	s.model = NewSnapshotBackupModel(
		dbProvider, s.mockJob, s.mockCoreCli, mockMgmtCli, s.mockImportCli, s.mockAccountCli, s.mockWorkflowModel,
		s.bucketRepo, s.backupRepo, s.scheduleRepo, s.restoreRepo, s.mockBackupExportRepo, s.mockBackupSettingModel, mockFeatureGate)
}

func (s *snapshotBackupModelTestSuit) TestQueryProjectClusterSnapshots() {
	testCases := []struct {
		name                string
		tenantID            uint64
		projectID           uint64
		backups             []entity.Backup
		mockCall            bool
		expectedSnapshotIDs []uint64
	}{
		{
			name:      "backups are nil",
			tenantID:  0,
			projectID: 0,
			backups:   nil,
			mockCall:  false,
		},
		{
			name:      "backups are empty",
			tenantID:  0,
			projectID: 0,
			backups:   []entity.Backup{},
			mockCall:  false,
		},
		{
			name:      "backups all lack cluster snapshot id",
			tenantID:  1,
			projectID: 1,
			backups: []entity.Backup{
				{
					ClusterSnapshotID: 0,
				},
			},
			mockCall: false,
		},
		{
			name:      "backups lack partial cluster snapshot id",
			tenantID:  1,
			projectID: 1,
			backups: []entity.Backup{
				{
					ClusterSnapshotID: 0,
				},
				{
					ClusterSnapshotID: 1,
				},
				{
					ClusterSnapshotID: 2,
				},
			},
			mockCall:            true,
			expectedSnapshotIDs: []uint64{1, 2},
		},
		{
			name:      "backups all have cluster snapshot id",
			tenantID:  1,
			projectID: 1,
			backups: []entity.Backup{
				{
					ClusterSnapshotID: 1,
				},
				{
					ClusterSnapshotID: 2,
				},
				{
					ClusterSnapshotID: 3,
				},
			},
			mockCall:            true,
			expectedSnapshotIDs: []uint64{1, 2, 3},
		},
	}

	for _, tc := range testCases {
		s.Run(tc.name, func() {
			if tc.mockCall {
				call := s.mockCoreCli.
					On("ListClusterSnapshot", mock.Anything, tc.tenantID, tc.projectID, tc.expectedSnapshotIDs).
					Return(nil, nil).Once()
				defer call.Unset()
			}
			_, err := s.model.queryProjectClusterSnapshots(context.Background(), tc.tenantID, tc.projectID, tc.backups)
			s.NoError(err)
		})
	}
}

func TestSnapshotBackupModel(t *testing.T) {
	suite.Run(t, new(snapshotBackupModelTestSuit))
}

func TestGroupBackupsByProjectID(t *testing.T) {
	testCases := []struct {
		name             string
		backups          []entity.Backup
		expectedGroupNum int
	}{
		{
			name:             "backups are nil",
			backups:          nil,
			expectedGroupNum: 0,
		},
		{
			name:             "backups are empty",
			backups:          []entity.Backup{},
			expectedGroupNum: 0,
		},
		{
			name: "backups are in one project",
			backups: []entity.Backup{
				{
					ProjectID: 1,
					ClusterID: 1,
				},
				{
					ProjectID: 1,
					ClusterID: 2,
				},
				{
					ProjectID: 1,
					ClusterID: 2,
				},
			},
			expectedGroupNum: 1,
		},
		{
			name: "backups are in multiple projects",
			backups: []entity.Backup{
				{
					ProjectID: 1,
					ClusterID: 1,
				},
				{
					ProjectID: 2,
					ClusterID: 2,
				},
				{
					ProjectID: 3,
					ClusterID: 3,
				},
			},
			expectedGroupNum: 3,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			projectBackups := groupBackupsByProjectID(tc.backups)
			assert.Equal(t, tc.expectedGroupNum, len(projectBackups))
		})
	}
}

func TestMergeClusterSnapshots(t *testing.T) {
	testCases := []struct {
		name           string
		map1           map[uint64]*core.ClusterSnapshot
		map2           map[uint64]*core.ClusterSnapshot
		expectedMapLen int
	}{
		{
			name:           "maps are nil",
			map1:           nil,
			map2:           nil,
			expectedMapLen: 0,
		},
		{
			name:           "maps are empty",
			map1:           map[uint64]*core.ClusterSnapshot{},
			map2:           map[uint64]*core.ClusterSnapshot{},
			expectedMapLen: 0,
		},
		{
			name: "maps have repetitive keys",
			map1: map[uint64]*core.ClusterSnapshot{
				1: {},
				2: {},
			},
			map2: map[uint64]*core.ClusterSnapshot{
				1: {},
				3: {},
			},
			expectedMapLen: 3,
		},
		{
			name: "maps have no repetitive keys",
			map1: map[uint64]*core.ClusterSnapshot{
				1: {},
				2: {},
			},
			map2: map[uint64]*core.ClusterSnapshot{
				3: {},
				4: {},
			},
			expectedMapLen: 4,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			clusterSnapshots := mergeClusterSnapshots(tc.map1, tc.map2)
			assert.Equal(t, tc.expectedMapLen, len(clusterSnapshots))
		})
	}
}

// ─── createDBEntities: RTCEnabled propagation ────────────────────────────────

// createAutoSchedule is a helper that inserts a valid BackupSchedule (AUTO type)
// into the fakeDB and returns its ID — used to satisfy createDBEntities' AUTO path
// without triggering the BackupCycle ENUM rejection on the MANUAL inline-create path.
func (s *snapshotBackupModelTestSuit) createAutoSchedule(clusterID uint64, name string) uint64 {
	now := time.Now()
	schedule := &entity.BackupSchedule{
		Type:           entity.BackupTypeAuto,
		Method:         entity.BackupMethodFull,
		BackupCycle:    entity.BackupCycleDaily,
		ClusterID:      entity.ID(clusterID),
		ClusterType:    entity.BackupClusterTypeDedicated,
		Name:           name,
		NextBackupTime: &now,
	}
	s.Require().NoError(s.scheduleRepo.Create(context.Background(), schedule))
	return schedule.ID.Uint64()
}

// TestCreateDBEntities_RTCEnabled verifies that createDBEntities stamps
// BucketSettingSnapshot.RTCEnabled from backup_setting.Bucket onto the new Backup
// entity so that snapshot-backup billing can see the correct RTC state.
func (s *snapshotBackupModelTestSuit) TestCreateDBEntities_RTCEnabled() {
	bucketURI := "s3://test-bucket"
	bucketPrefix := "test-prefix"
	rtcTrue := true
	rtcFalse := false

	tests := map[string]struct {
		clusterID      uint64
		bucketID       uint64
		snapshotID     uint64
		rtcEnabled     *bool
		wantRTCEnabled *bool
	}{
		"rtc enabled: propagated to snapshot backup":  {clusterID: 10, bucketID: 99, snapshotID: 2000, rtcEnabled: &rtcTrue, wantRTCEnabled: pointer.To(true)},
		"rtc disabled: propagated to snapshot backup": {clusterID: 20, bucketID: 98, snapshotID: 2001, rtcEnabled: &rtcFalse, wantRTCEnabled: pointer.To(false)},
		"rtc nil: preserved as nil":                   {clusterID: 30, bucketID: 97, snapshotID: 2002, rtcEnabled: nil, wantRTCEnabled: nil},
	}

	for name, tc := range tests {
		s.Run(name, func() {
			scheduleID := s.createAutoSchedule(tc.clusterID, "sched-"+name)
			cluster := &mgmt.Cluster{
				ClusterId:      tc.clusterID,
				OrganizationId: 2,
				ProjectId:      3,
				CloudProvider:  "aws",
				Region:         "us-east-1",
				ClusterInfo: &mgmt.Cluster_DedicatedCluster{
					DedicatedCluster: &dedicatedv1.Cluster{Version: "v7.5.0"},
				},
			}
			setting := &entity.BackupSetting{
				Updatable: entity.Updatable{Base: entity.Base{ID: entity.ID(tc.clusterID)}},
				Status:    entity.BackupSettingStatusNormal,
				Bucket: &entity.BucketSetting{
					Prefix:     &bucketPrefix,
					RTCEnabled: tc.rtcEnabled,
					Buckets:    []entity.BackupSettingBucket{{Master: true, BucketId: tc.bucketID, Regions: []string{"us-east-1"}}},
				},
			}

			s.mockCoreCli.On("CreateClusterSnapshot", mock.Anything, tc.clusterID).
				Return(tc.snapshotID, nil).Once()
			s.mockBackupSettingModel.On("QueryMasterBucket", mock.Anything, mock.Anything).
				Return(&domain.QueryMasterBucketRespV2{BucketURI: &bucketURI}, nil).Once()

			pres := &preCheckResp{cluster: cluster, setting: setting}
			req := &domain.AsyncCreateBackupReq{
				ClusterID:            tc.clusterID,
				UserID:               1,
				BackupType:           dataflow.BackupType_AUTO,
				AutoBackupScheduleID: &scheduleID,
			}

			backup, err := s.model.createDBEntities(context.Background(), zap.NewNop(), pres, req)
			s.Require().NoError(err)
			s.Require().NotNil(backup.BucketSettingSnapshot)
			s.Equal(tc.wantRTCEnabled, backup.BucketSettingSnapshot.RTCEnabled)
		})
	}
}
