package impl

import (
	"context"
	"testing"

	dagentity "github.com/shiningrush/fastflow/pkg/entity"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/suite"
	"github.com/tidbcloud/dataflow-service/app/models/domain"
	backupRepo "github.com/tidbcloud/dataflow-service/app/repo/backup"
	crrfRepoMock "github.com/tidbcloud/dataflow-service/app/repo/crrf/mock"
	"github.com/tidbcloud/dataflow-service/app/repo/entity"
	repoMock "github.com/tidbcloud/dataflow-service/app/repo/mock"
	wfMock "github.com/tidbcloud/dataflow-service/app/workflow/mock"
	bizerr_wrapper "github.com/tidbcloud/dataflow-service/pkg/bizerr-wrapper"
	"github.com/tidbcloud/pkgv2/bizerr"
)

// ---------- suite ----------

type crrfModelTestSuite struct {
	suite.Suite

	backupSettingRepo *repoMock.BackupSettingRepo
	logBackupRepo     *repoMock.LogBackupRepo
	crrfSettingRepo   *crrfRepoMock.CRRFSettingRepo
	wfClient          *wfMock.Workflow

	model *crrfModelImpl
}

func TestCRRFModelSuite(t *testing.T) {
	suite.Run(t, new(crrfModelTestSuite))
}

func (s *crrfModelTestSuite) SetupSuite() {
	s.backupSettingRepo = repoMock.NewBackupSettingRepo(s.T())
	s.logBackupRepo = repoMock.NewLogBackupRepo(s.T())
	s.crrfSettingRepo = crrfRepoMock.NewCRRFSettingRepo(s.T())
	s.wfClient = wfMock.NewWorkflow(s.T())

	s.model = &crrfModelImpl{
		backupSettingRepo: s.backupSettingRepo,
		logBackupRepo:     s.logBackupRepo,
		crrfSettingRepo:   s.crrfSettingRepo,
		workflowClient:    s.wfClient,
	}
}

func (s *crrfModelTestSuite) TearDownTest() {
	s.backupSettingRepo.AssertExpectations(s.T())
	s.logBackupRepo.AssertExpectations(s.T())
	s.crrfSettingRepo.AssertExpectations(s.T())
	s.wfClient.AssertExpectations(s.T())
}

// ---------- helpers ----------

func fakeDagIns() *dagentity.DagInstance {
	return &dagentity.DagInstance{BaseInfo: dagentity.BaseInfo{ID: "fake-dag-ins"}}
}

func fakeLogBackup(id entity.ID) entity.LogBackup {
	lb := entity.LogBackup{}
	lb.ID = id
	lb.Status = entity.LogBackupStatusRunning
	return lb
}

func awsBackupSetting() *entity.BackupSetting {
	bs := &entity.BackupSetting{
		Status:            entity.BackupSettingStatusNormal,
		InfraProviderType: string(domain.InfraProviderTypeAWS),
		PiTR:              &entity.PiTRSetting{Enable: true},
		Bucket:            dualRegionBucketSetting("us-east-1", "us-west-2"),
	}
	bs.ID = 10
	return bs
}

func enabledCRRFSetting(clusterID uint64) *entity.CRRFSetting {
	cs := &entity.CRRFSetting{
		ClusterID:       entity.ID(clusterID),
		Status:          entity.CRRFSettingStatusEnabled,
		SecondaryRegion: "us-west-2",
		MasterRegion:    "us-east-1",
		MasterBucketID:  entity.ID(99),
	}
	cs.ID = 5
	return cs
}

func dualRegionBucketSetting(primaryRegion, secondaryRegion string) *entity.BucketSetting {
	return &entity.BucketSetting{
		DualRegionEnable: true,
		Buckets: []entity.BackupSettingBucket{
			{Master: true, BucketId: 11, Regions: []string{primaryRegion}},
			{Master: false, BucketId: 22, Regions: []string{secondaryRegion}},
		},
	}
}

// ---------- EnableCRRF ----------

func (s *crrfModelTestSuite) TestEnableCRRF() {
	ctx := context.Background()
	const clusterID = uint64(1001)

	tests := map[string]struct {
		req   *domain.EnableCRRFReq
		setup func()
		check func(err bizerr.BizError)
	}{
		"success - creates new CRRF setting": {
			req: &domain.EnableCRRFReq{ClusterID: clusterID, SecondaryRegion: "us-west-2"},
			setup: func() {
				s.backupSettingRepo.On("GetByFilter", ctx, mock.Anything).
					Return(awsBackupSetting(), nil).Once()
				s.crrfSettingRepo.On("GetByClusterID", ctx, clusterID).
					Return(nil, nil).Once()
				s.logBackupRepo.On("List", mock.Anything, mock.Anything).
					Return([]entity.LogBackup{fakeLogBackup(501)}, nil).Once()
				s.crrfSettingRepo.On("Save", ctx, mock.MatchedBy(func(cs *entity.CRRFSetting) bool {
					return cs.Status == entity.CRRFSettingStatusEnabling &&
						cs.SecondaryRegion == "us-west-2"
				})).Return(nil).Once()
				s.wfClient.On("RunDagWithTags", mock.Anything, mock.Anything, mock.Anything).
					Return(fakeDagIns(), nil).Once()
			},
			check: func(err bizerr.BizError) { s.Nil(err) },
		},
		"success - re-enables existing disabled CRRF setting": {
			req: &domain.EnableCRRFReq{ClusterID: clusterID, SecondaryRegion: "ap-southeast-1"},
			setup: func() {
				bs := awsBackupSetting()
				bs.Bucket = dualRegionBucketSetting("us-east-1", "ap-southeast-1")
				s.backupSettingRepo.On("GetByFilter", ctx, mock.Anything).
					Return(bs, nil).Once()
				existing := &entity.CRRFSetting{
					ClusterID: entity.ID(clusterID),
					Status:    entity.CRRFSettingStatusDisabled,
				}
				existing.ID = 3
				s.crrfSettingRepo.On("GetByClusterID", ctx, clusterID).
					Return(existing, nil).Once()
				s.logBackupRepo.On("List", mock.Anything, mock.Anything).
					Return([]entity.LogBackup{fakeLogBackup(502)}, nil).Once()
				s.crrfSettingRepo.On("Save", ctx, mock.MatchedBy(func(cs *entity.CRRFSetting) bool {
					return cs.Status == entity.CRRFSettingStatusEnabling
				})).Return(nil).Once()
				s.wfClient.On("RunDagWithTags", mock.Anything, mock.Anything, mock.Anything).
					Return(fakeDagIns(), nil).Once()
			},
			check: func(err bizerr.BizError) { s.Nil(err) },
		},
		"error - secondary_region is empty": {
			req:   &domain.EnableCRRFReq{ClusterID: clusterID, SecondaryRegion: ""},
			setup: func() {},
			check: func(err bizerr.BizError) {
				s.NotNil(err)
				s.Contains(err.Error(), "secondary_region")
			},
		},
		"error - backup setting not found": {
			req: &domain.EnableCRRFReq{ClusterID: clusterID, SecondaryRegion: "us-west-2"},
			setup: func() {
				s.backupSettingRepo.On("GetByFilter", ctx, mock.Anything).
					Return(nil, nil).Once()
			},
			check: func(err bizerr.BizError) {
				s.NotNil(err)
				s.Contains(err.Error(), "backup setting not found")
			},
		},
		"error - backup setting not in normal state": {
			req: &domain.EnableCRRFReq{ClusterID: clusterID, SecondaryRegion: "us-west-2"},
			setup: func() {
				bs := awsBackupSetting()
				bs.Status = entity.BackupSettingStatusCreating
				s.backupSettingRepo.On("GetByFilter", ctx, mock.Anything).
					Return(bs, nil).Once()
			},
			check: func(err bizerr.BizError) { s.NotNil(err) },
		},
		"error - non-AWS provider": {
			req: &domain.EnableCRRFReq{ClusterID: clusterID, SecondaryRegion: "us-west-2"},
			setup: func() {
				bs := awsBackupSetting()
				bs.InfraProviderType = string(domain.InfraProviderTypeGCP)
				s.backupSettingRepo.On("GetByFilter", ctx, mock.Anything).
					Return(bs, nil).Once()
			},
			check: func(err bizerr.BizError) {
				s.NotNil(err)
				s.Contains(err.Error(), "AWS")
			},
		},
		"error - CRRF already active (enabling)": {
			req: &domain.EnableCRRFReq{ClusterID: clusterID, SecondaryRegion: "us-west-2"},
			setup: func() {
				s.backupSettingRepo.On("GetByFilter", ctx, mock.Anything).
					Return(awsBackupSetting(), nil).Once()
				s.crrfSettingRepo.On("GetByClusterID", ctx, clusterID).
					Return(&entity.CRRFSetting{
						ClusterID: entity.ID(clusterID),
						Status:    entity.CRRFSettingStatusEnabling,
					}, nil).Once()
			},
			check: func(err bizerr.BizError) { s.NotNil(err) },
		},
		"error - dual-region region mismatch": {
			req: &domain.EnableCRRFReq{ClusterID: clusterID, SecondaryRegion: "eu-west-1"},
			setup: func() {
				bs := awsBackupSetting()
				bs.Bucket = dualRegionBucketSetting("us-east-1", "us-west-2")
				s.backupSettingRepo.On("GetByFilter", ctx, mock.Anything).
					Return(bs, nil).Once()
				s.crrfSettingRepo.On("GetByClusterID", ctx, clusterID).
					Return(nil, nil).Once()
			},
			check: func(err bizerr.BizError) {
				s.NotNil(err)
				s.Contains(err.Error(), "dual-region")
			},
		},
		"error - workflow start fails and reverts setting": {
			req: &domain.EnableCRRFReq{ClusterID: clusterID, SecondaryRegion: "us-west-2"},
			setup: func() {
				s.backupSettingRepo.On("GetByFilter", ctx, mock.Anything).
					Return(awsBackupSetting(), nil).Once()
				s.crrfSettingRepo.On("GetByClusterID", ctx, clusterID).
					Return(nil, nil).Once()
				s.logBackupRepo.On("List", mock.Anything, mock.Anything).
					Return([]entity.LogBackup{fakeLogBackup(503)}, nil).Once()
				s.crrfSettingRepo.On("Save", ctx, mock.MatchedBy(func(cs *entity.CRRFSetting) bool {
					return cs.Status == entity.CRRFSettingStatusEnabling
				})).Return(nil).Once()
				s.wfClient.On("RunDagWithTags", mock.Anything, mock.Anything, mock.Anything).
					Return(nil, bizerr_wrapper.NewInternalErr("dag start failed")).Once()
				s.crrfSettingRepo.On("Save", ctx, mock.MatchedBy(func(cs *entity.CRRFSetting) bool {
					return cs.Status == entity.CRRFSettingStatusDisabled && cs.ErrMsg != nil
				})).Return(nil).Once()
			},
			check: func(err bizerr.BizError) {
				s.NotNil(err)
				s.Contains(err.Error(), "workflow")
			},
		},
		"error - crrfSettingRepo.GetByClusterID fails": {
			req: &domain.EnableCRRFReq{ClusterID: clusterID, SecondaryRegion: "us-west-2"},
			setup: func() {
				s.backupSettingRepo.On("GetByFilter", ctx, mock.Anything).
					Return(awsBackupSetting(), nil).Once()
				s.crrfSettingRepo.On("GetByClusterID", ctx, clusterID).
					Return(nil, bizerr_wrapper.NewInternalErr("db error")).Once()
			},
			check: func(err bizerr.BizError) { s.NotNil(err) },
		},
		"error - backupSettingRepo.GetByFilter fails": {
			req: &domain.EnableCRRFReq{ClusterID: clusterID, SecondaryRegion: "us-east-1"},
			setup: func() {
				s.backupSettingRepo.On("GetByFilter", ctx, mock.Anything).
					Return(nil, bizerr_wrapper.NewInternalErr("db error")).Once()
			},
			check: func(err bizerr.BizError) { s.NotNil(err) },
		},
	}

	for name, tc := range tests {
		s.Run(name, func() {
			tc.setup()
			err := s.model.EnableCRRF(ctx, tc.req)
			tc.check(err)
		})
	}
}

// ---------- EnableCRRF — dual-region already enabled ----------

func (s *crrfModelTestSuite) TestEnableCRRF_DualRegionAlreadyEnabled() {
	ctx := context.Background()
	const clusterID = uint64(4001)

	bs := awsBackupSetting()
	bs.Bucket = dualRegionBucketSetting("us-east-1", "us-west-2")

	s.backupSettingRepo.On("GetByFilter", ctx, mock.Anything).Return(bs, nil).Once()
	s.crrfSettingRepo.On("GetByClusterID", ctx, clusterID).Return(nil, nil).Once()
	s.logBackupRepo.On("List", mock.Anything, mock.Anything).Return([]entity.LogBackup{fakeLogBackup(521)}, nil).Once()
	s.crrfSettingRepo.On("Save", ctx, mock.MatchedBy(func(cs *entity.CRRFSetting) bool {
		return cs.MasterBucketID == entity.ID(11) && cs.SecondaryBucketID == entity.ID(22)
	})).Return(nil).Once()
	s.wfClient.On("RunDagWithTags", mock.Anything, mock.Anything, mock.Anything).
		Return(fakeDagIns(), nil).Once()

	err := s.model.EnableCRRF(ctx, &domain.EnableCRRFReq{
		ClusterID: clusterID, SecondaryRegion: "us-west-2",
	})
	s.Nil(err)
}

// ---------- EnableCRRF — EKS provider is allowed ----------

func (s *crrfModelTestSuite) TestEnableCRRF_EKSIsAllowed() {
	ctx := context.Background()
	const clusterID = uint64(6001)

	bs := awsBackupSetting()
	bs.InfraProviderType = string(domain.InfraProviderTypeEKS)
	bs.Bucket = dualRegionBucketSetting("us-east-1", "ap-southeast-1")
	s.backupSettingRepo.On("GetByFilter", ctx, mock.Anything).Return(bs, nil).Once()
	s.crrfSettingRepo.On("GetByClusterID", ctx, clusterID).Return(nil, nil).Once()
	s.logBackupRepo.On("List", mock.Anything, mock.Anything).Return([]entity.LogBackup{fakeLogBackup(522)}, nil).Once()
	s.crrfSettingRepo.On("Save", ctx, mock.Anything).Return(nil).Once()
	s.wfClient.On("RunDagWithTags", mock.Anything, mock.Anything, mock.Anything).
		Return(fakeDagIns(), nil).Once()

	err := s.model.EnableCRRF(ctx, &domain.EnableCRRFReq{
		ClusterID: clusterID, SecondaryRegion: "ap-southeast-1",
	})
	s.Nil(err)
}

// ---------- EnableCRRF — filter carries correct clusterID ----------

func (s *crrfModelTestSuite) TestEnableCRRF_FilterCarriesClusterID() {
	ctx := context.Background()
	const clusterID = uint64(8001)

	s.backupSettingRepo.On("GetByFilter", ctx, mock.MatchedBy(func(f *backupRepo.BackupSettingFilter) bool {
		return f.ClusterID != nil && *f.ClusterID == clusterID
	})).Return(nil, nil).Once()

	err := s.model.EnableCRRF(ctx, &domain.EnableCRRFReq{
		ClusterID: clusterID, SecondaryRegion: "us-east-1",
	})
	s.NotNil(err)
	s.Contains(err.Error(), "not found")
}

// ---------- EnableCRRF — stale bucket IDs are cleared ----------

func (s *crrfModelTestSuite) TestEnableCRRF_StaleButketIDsAreCleared() {
	ctx := context.Background()
	const clusterID = uint64(9001)

	bs := awsBackupSetting()
	bs.PiTR = &entity.PiTRSetting{Enable: true}
	bs.Bucket = &entity.BucketSetting{
		DualRegionEnable: false,
		Buckets: []entity.BackupSettingBucket{
			{Master: true, BucketId: 11, Regions: []string{"us-east-1"}},
		},
	}

	// Simulate a previously failed enable that left stale bucket IDs.
	existing := &entity.CRRFSetting{
		ClusterID:         entity.ID(clusterID),
		Status:            entity.CRRFSettingStatusDisabled,
		MasterBucketID:    entity.ID(77),
		SecondaryBucketID: entity.ID(88),
	}
	existing.ID = 7

	s.backupSettingRepo.On("GetByFilter", ctx, mock.Anything).Return(bs, nil).Once()
	s.crrfSettingRepo.On("GetByClusterID", ctx, clusterID).Return(existing, nil).Once()
	s.logBackupRepo.On("List", mock.Anything, mock.Anything).Return([]entity.LogBackup{fakeLogBackup(530)}, nil).Once()
	s.crrfSettingRepo.On("Save", ctx, mock.MatchedBy(func(cs *entity.CRRFSetting) bool {
		// Stale bucket IDs must be zeroed before the enable DAG runs.
		return cs.MasterBucketID == 0 && cs.SecondaryBucketID == 0 &&
			cs.Status == entity.CRRFSettingStatusEnabling
	})).Return(nil).Once()
	s.wfClient.On("RunDagWithTags", mock.Anything, mock.Anything, mock.Anything).
		Return(fakeDagIns(), nil).Once()

	err := s.model.EnableCRRF(ctx, &domain.EnableCRRFReq{
		ClusterID: clusterID, SecondaryRegion: "us-west-2",
	})
	s.Nil(err)
}

// ---------- EnableCRRF — relaxed status check ----------

func (s *crrfModelTestSuite) Test_EnableCRRF_BackupSettingEditingPitr_Allowed() {
	ctx := context.Background()
	clusterID := uint64(101)
	bs := awsBackupSetting()
	bs.Status = entity.BackupSettingStatusEditingPitr
	bs.PiTR = &entity.PiTRSetting{Enable: true}
	bs.Bucket = dualRegionBucketSetting("us-east-1", "us-west-2")
	s.backupSettingRepo.On("GetByFilter", ctx, mock.Anything).Return(bs, nil).Once()
	s.crrfSettingRepo.On("GetByClusterID", ctx, clusterID).Return(nil, bizerr.New(bizerr.ErrCodeRecordNotFound, "not found")).Once()
	s.logBackupRepo.On("List", mock.Anything, mock.Anything).Return([]entity.LogBackup{fakeLogBackup(511)}, nil).Once()
	s.crrfSettingRepo.On("Save", ctx, mock.Anything).Return(nil).Once()
	s.wfClient.On("RunDagWithTags", mock.Anything, mock.Anything, mock.Anything).Return(fakeDagIns(), nil).Once()

	req := &domain.EnableCRRFReq{ClusterID: clusterID, SecondaryRegion: "us-west-2"}
	err := s.model.EnableCRRF(ctx, req)
	s.Nil(err)
}

func (s *crrfModelTestSuite) Test_EnableCRRF_BackupSettingEditingRegion_Allowed() {
	ctx := context.Background()
	clusterID := uint64(102)
	bs := awsBackupSetting()
	bs.Status = entity.BackupSettingStatusEditingRegion
	bs.PiTR = &entity.PiTRSetting{Enable: true}
	bs.Bucket = dualRegionBucketSetting("us-east-1", "us-west-2")
	s.backupSettingRepo.On("GetByFilter", ctx, mock.Anything).Return(bs, nil).Once()
	s.crrfSettingRepo.On("GetByClusterID", ctx, clusterID).Return(nil, bizerr.New(bizerr.ErrCodeRecordNotFound, "not found")).Once()
	s.logBackupRepo.On("List", mock.Anything, mock.Anything).Return([]entity.LogBackup{fakeLogBackup(512)}, nil).Once()
	s.crrfSettingRepo.On("Save", ctx, mock.Anything).Return(nil).Once()
	s.wfClient.On("RunDagWithTags", mock.Anything, mock.Anything, mock.Anything).Return(fakeDagIns(), nil).Once()

	req := &domain.EnableCRRFReq{ClusterID: clusterID, SecondaryRegion: "us-west-2"}
	err := s.model.EnableCRRF(ctx, req)
	s.Nil(err)
}

// ---------- QueryCRRF ----------

func (s *crrfModelTestSuite) TestQueryCRRF() {
	ctx := context.Background()
	const clusterID = uint64(2001)

	tests := map[string]struct {
		setup func()
		check func(info *domain.CRRFInfo, err bizerr.BizError)
	}{
		"success - no CRRF setting returns disabled": {
			setup: func() {
				s.crrfSettingRepo.On("GetByClusterID", ctx, clusterID).
					Return(nil, nil).Once()
			},
			check: func(info *domain.CRRFInfo, err bizerr.BizError) {
				s.Nil(err)
				s.Equal(entity.CRRFSettingStatusDisabled, info.Status)
				s.Empty(info.SecondaryRegion)
			},
		},
		"success - returns enabling status with secondary region": {
			setup: func() {
				s.crrfSettingRepo.On("GetByClusterID", ctx, clusterID).
					Return(&entity.CRRFSetting{
						ClusterID:       entity.ID(clusterID),
						Status:          entity.CRRFSettingStatusEnabling,
						SecondaryRegion: "us-west-2",
						MasterRegion:    "us-east-1",
					}, nil).Once()
			},
			check: func(info *domain.CRRFInfo, err bizerr.BizError) {
				s.Nil(err)
				s.Equal(entity.CRRFSettingStatusEnabling, info.Status)
				s.Equal("us-west-2", info.SecondaryRegion)
				s.Equal("us-east-1", info.MasterRegion)
			},
		},
		"success - error message propagated from disabled setting": {
			setup: func() {
				msg := "enable failed due to network error"
				s.crrfSettingRepo.On("GetByClusterID", ctx, clusterID).
					Return(&entity.CRRFSetting{
						Status: entity.CRRFSettingStatusDisabled,
						ErrMsg: &msg,
					}, nil).Once()
			},
			check: func(info *domain.CRRFInfo, err bizerr.BizError) {
				s.Nil(err)
				s.Equal("enable failed due to network error", info.ErrorMessage)
			},
		},
		"success - bucket IDs returned when set": {
			setup: func() {
				s.crrfSettingRepo.On("GetByClusterID", ctx, clusterID).
					Return(&entity.CRRFSetting{
						ClusterID:         entity.ID(clusterID),
						Status:            entity.CRRFSettingStatusEnabled,
						MasterBucketID:    entity.ID(11),
						SecondaryBucketID: entity.ID(22),
					}, nil).Once()
			},
			check: func(info *domain.CRRFInfo, err bizerr.BizError) {
				s.Nil(err)
				s.Equal(uint64(11), info.MasterBucketID)
				s.Equal(uint64(22), info.SecondaryBucketID)
			},
		},
		"error - repo failure": {
			setup: func() {
				s.crrfSettingRepo.On("GetByClusterID", ctx, clusterID).
					Return(nil, bizerr_wrapper.NewInternalErr("db error")).Once()
			},
			check: func(info *domain.CRRFInfo, err bizerr.BizError) {
				s.Nil(info)
				s.NotNil(err)
			},
		},
	}

	for name, tc := range tests {
		s.Run(name, func() {
			tc.setup()
			info, err := s.model.QueryCRRF(ctx, clusterID)
			tc.check(info, err)
		})
	}
}

// ---------- DisableCRRF ----------

func (s *crrfModelTestSuite) TestDisableCRRF() {
	ctx := context.Background()
	const clusterID = uint64(3001)

	tests := map[string]struct {
		setup func()
		check func(err bizerr.BizError)
	}{
		"success - transitions enabled CRRF to disabling and starts DAG": {
			setup: func() {
				s.crrfSettingRepo.On("GetByClusterID", ctx, clusterID).
					Return(enabledCRRFSetting(clusterID), nil).Once()
				s.crrfSettingRepo.On("Save", ctx, mock.MatchedBy(func(cs *entity.CRRFSetting) bool {
					return cs.Status == entity.CRRFSettingStatusDisabling
				})).Return(nil).Once()
				s.wfClient.On("RunDagWithTags", mock.Anything, mock.Anything, mock.Anything).
					Return(fakeDagIns(), nil).Once()
			},
			check: func(err bizerr.BizError) { s.Nil(err) },
		},
		"error - CRRF setting not found": {
			setup: func() {
				s.crrfSettingRepo.On("GetByClusterID", ctx, clusterID).
					Return(nil, nil).Once()
			},
			check: func(err bizerr.BizError) {
				s.NotNil(err)
				s.Contains(err.Error(), "not configured")
			},
		},
		"error - CRRF is enabling (not yet enabled)": {
			setup: func() {
				s.crrfSettingRepo.On("GetByClusterID", ctx, clusterID).
					Return(&entity.CRRFSetting{
						ClusterID: entity.ID(clusterID),
						Status:    entity.CRRFSettingStatusEnabling,
					}, nil).Once()
			},
			check: func(err bizerr.BizError) {
				s.NotNil(err)
				s.Contains(err.Error(), string(entity.CRRFSettingStatusEnabled))
			},
		},
		"error - CRRF already disabled": {
			setup: func() {
				s.crrfSettingRepo.On("GetByClusterID", ctx, clusterID).
					Return(&entity.CRRFSetting{Status: entity.CRRFSettingStatusDisabled}, nil).Once()
			},
			check: func(err bizerr.BizError) {
				s.NotNil(err)
				s.Contains(err.Error(), string(entity.CRRFSettingStatusEnabled))
			},
		},
		"error - CRRF is disabling": {
			setup: func() {
				s.crrfSettingRepo.On("GetByClusterID", ctx, clusterID).
					Return(&entity.CRRFSetting{
						ClusterID: entity.ID(clusterID),
						Status:    entity.CRRFSettingStatusDisabling,
					}, nil).Once()
			},
			check: func(err bizerr.BizError) {
				s.NotNil(err)
				s.Contains(err.Error(), string(entity.CRRFSettingStatusEnabled))
			},
		},
		"error - CRRF is failed": {
			setup: func() {
				s.crrfSettingRepo.On("GetByClusterID", ctx, clusterID).
					Return(&entity.CRRFSetting{
						ClusterID: entity.ID(clusterID),
						Status:    entity.CRRFSettingStatusFailed,
					}, nil).Once()
			},
			check: func(err bizerr.BizError) {
				s.NotNil(err)
				s.Contains(err.Error(), string(entity.CRRFSettingStatusEnabled))
			},
		},
		"error - save to disabling fails": {
			setup: func() {
				s.crrfSettingRepo.On("GetByClusterID", ctx, clusterID).
					Return(enabledCRRFSetting(clusterID), nil).Once()
				s.crrfSettingRepo.On("Save", ctx, mock.Anything).
					Return(bizerr_wrapper.NewInternalErr("db save failed")).Once()
			},
			check: func(err bizerr.BizError) { s.NotNil(err) },
		},
		"error - DAG start fails and marks failed": {
			setup: func() {
				s.crrfSettingRepo.On("GetByClusterID", ctx, clusterID).
					Return(enabledCRRFSetting(clusterID), nil).Once()
				s.crrfSettingRepo.On("Save", ctx, mock.MatchedBy(func(cs *entity.CRRFSetting) bool {
					return cs.Status == entity.CRRFSettingStatusDisabling
				})).Return(nil).Once()
				s.wfClient.On("RunDagWithTags", mock.Anything, mock.Anything, mock.Anything).
					Return(nil, bizerr_wrapper.NewInternalErr("dag start failed")).Once()
				// markCRRFFailedWithError saves with failed status
				s.crrfSettingRepo.On("Save", ctx, mock.MatchedBy(func(cs *entity.CRRFSetting) bool {
					return cs.Status == entity.CRRFSettingStatusFailed && cs.ErrMsg != nil
				})).Return(nil).Once()
			},
			check: func(err bizerr.BizError) {
				s.NotNil(err)
				s.Contains(err.Error(), "workflow")
			},
		},
		"error - repo lookup fails": {
			setup: func() {
				s.crrfSettingRepo.On("GetByClusterID", ctx, clusterID).
					Return(nil, bizerr_wrapper.NewInternalErr("db error")).Once()
			},
			check: func(err bizerr.BizError) { s.NotNil(err) },
		},
	}

	for name, tc := range tests {
		s.Run(name, func() {
			tc.setup()
			err := s.model.DisableCRRF(ctx, &domain.DisableCRRFReq{ClusterID: clusterID})
			tc.check(err)
		})
	}
}

// ---------- extractSecondaryRegion ----------

func (s *crrfModelTestSuite) TestExtractSecondaryRegion() {
	m := &crrfModelImpl{}

	tests := map[string]struct {
		bs     *entity.BackupSetting
		expect string
	}{
		"nil bucket returns empty": {
			bs:     &entity.BackupSetting{},
			expect: "",
		},
		"returns secondary bucket first region": {
			bs: &entity.BackupSetting{
				Bucket: &entity.BucketSetting{
					Buckets: []entity.BackupSettingBucket{
						{Master: true, Regions: []string{"us-east-1"}},
						{Master: false, Regions: []string{"us-west-2"}},
					},
				},
			},
			expect: "us-west-2",
		},
		"no secondary bucket returns empty": {
			bs: &entity.BackupSetting{
				Bucket: &entity.BucketSetting{
					Buckets: []entity.BackupSettingBucket{
						{Master: true, Regions: []string{"us-east-1"}},
					},
				},
			},
			expect: "",
		},
		"secondary bucket with no regions returns empty": {
			bs: &entity.BackupSetting{
				Bucket: &entity.BucketSetting{
					Buckets: []entity.BackupSettingBucket{
						{Master: false, Regions: []string{}},
					},
				},
			},
			expect: "",
		},
	}

	for name, tc := range tests {
		s.Run(name, func() {
			s.Equal(tc.expect, m.extractSecondaryRegion(tc.bs))
		})
	}
}

// ---------- populateBucketIDs ----------

func (s *crrfModelTestSuite) TestPopulateBucketIDs() {
	m := &crrfModelImpl{}

	tests := map[string]struct {
		bs           *entity.BackupSetting
		expectMaster entity.ID
		expectSecond entity.ID
	}{
		"nil bucket is a no-op": {
			bs:           &entity.BackupSetting{},
			expectMaster: 0,
			expectSecond: 0,
		},
		"populates master and secondary bucket IDs": {
			bs: &entity.BackupSetting{
				Bucket: &entity.BucketSetting{
					Buckets: []entity.BackupSettingBucket{
						{Master: true, BucketId: 10},
						{Master: false, BucketId: 20},
					},
				},
			},
			expectMaster: 10,
			expectSecond: 20,
		},
	}

	for name, tc := range tests {
		s.Run(name, func() {
			crrf := &entity.CRRFSetting{}
			m.populateBucketIDs(crrf, tc.bs)
			s.Equal(tc.expectMaster, crrf.MasterBucketID)
			s.Equal(tc.expectSecond, crrf.SecondaryBucketID)
		})
	}
}

// ---------- CRRFSetting.IsActive ----------

func (s *crrfModelTestSuite) TestCRRFSettingIsActive() {
	tests := map[string]struct {
		status entity.CRRFSettingStatus
		active bool
	}{
		"disabled is not active": {entity.CRRFSettingStatusDisabled, false},
		"enabling is active":     {entity.CRRFSettingStatusEnabling, true},
		"enabled is active":      {entity.CRRFSettingStatusEnabled, true},
	}
	for name, tc := range tests {
		s.Run(name, func() {
			cs := &entity.CRRFSetting{Status: tc.status}
			s.Equal(tc.active, cs.IsActive())
		})
	}
}
