package enable

import (
	"context"
	"testing"

	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/suite"
	backupModelMock "github.com/tidbcloud/dataflow-service/app/models/backup/mock"
	"github.com/tidbcloud/dataflow-service/app/repo/entity"
	repoMock "github.com/tidbcloud/dataflow-service/app/repo/mock"
	"github.com/tidbcloud/dataflow-service/app/workflow/action"
	"github.com/tidbcloud/pkgv2/bizerr"
	"go.uber.org/zap"
)

type enablePitrTestSuite struct {
	suite.Suite
	act                *enablePitrAction
	backupSettingRepo  *repoMock.BackupSettingRepo
	backupSettingModel *backupModelMock.BackupSettingModel
	logBackupRepo      *repoMock.LogBackupRepo
	crrfSettingRepo    *repoMock.CRRFSettingRepo
	logger             *zap.Logger
}

func TestEnablePitrSuite(t *testing.T) {
	suite.Run(t, new(enablePitrTestSuite))
}

func (s *enablePitrTestSuite) SetupSuite() {
	s.backupSettingRepo = repoMock.NewBackupSettingRepo(s.T())
	s.backupSettingModel = backupModelMock.NewBackupSettingModel(s.T())
	s.logBackupRepo = repoMock.NewLogBackupRepo(s.T())
	s.crrfSettingRepo = repoMock.NewCRRFSettingRepo(s.T())
	s.logger = zap.NewNop()
	s.act = &enablePitrAction{
		backupSettingRepo:  s.backupSettingRepo,
		backupSettingModel: s.backupSettingModel,
		logBackupRepo:      s.logBackupRepo,
		crrfSettingRepo:    s.crrfSettingRepo,
	}
}

func (s *enablePitrTestSuite) TearDownTest() {
	s.backupSettingRepo.AssertExpectations(s.T())
	s.backupSettingModel.AssertExpectations(s.T())
	s.logBackupRepo.AssertExpectations(s.T())
	s.crrfSettingRepo.AssertExpectations(s.T())
}

func (s *enablePitrTestSuite) params() *enablePitrParams {
	return &enablePitrParams{ClusterID: 1001}
}

// --- Run tests ---

func (s *enablePitrTestSuite) TestRun() {
	ctx := context.Background()
	p := s.params()

	tests := map[string]struct {
		setup   func()
		wantErr bool
		errIs   error
	}{
		"repo error": {
			setup: func() {
				s.backupSettingRepo.On("GetByFilter", mock.Anything, mock.Anything).
					Return(nil, bizerr.New(bizerr.ErrCodeDB, "db error")).Once()
			},
			wantErr: true,
		},
		"backup setting not found": {
			setup: func() {
				s.backupSettingRepo.On("GetByFilter", mock.Anything, mock.Anything).Return(nil, nil).Once()
			},
			wantErr: true,
			errIs:   action.StopRetry,
		},
		"PiTR already enabled — idempotent skip": {
			setup: func() {
				bs := &entity.BackupSetting{PiTR: &entity.PiTRSetting{Enable: true}}
				s.backupSettingRepo.On("GetByFilter", mock.Anything, mock.Anything).Return(bs, nil).Once()
			},
			wantErr: false,
		},
		"trigger succeeds": {
			setup: func() {
				bs := &entity.BackupSetting{PiTR: &entity.PiTRSetting{Enable: false}}
				s.backupSettingRepo.On("GetByFilter", mock.Anything, mock.Anything).Return(bs, nil).Once()
				s.backupSettingModel.On("ModifyPitBackupSetting", mock.Anything, mock.Anything).Return(nil).Once()
			},
			wantErr: false,
		},
		"trigger fails": {
			setup: func() {
				bs := &entity.BackupSetting{PiTR: &entity.PiTRSetting{Enable: false}}
				s.backupSettingRepo.On("GetByFilter", mock.Anything, mock.Anything).Return(bs, nil).Once()
				s.backupSettingModel.On("ModifyPitBackupSetting", mock.Anything, mock.Anything).
					Return(bizerr.New(bizerr.ErrCodeUnknown, "remote error")).Once()
			},
			wantErr: true,
		},
		"PiTR field nil — triggers enable": {
			setup: func() {
				bs := &entity.BackupSetting{PiTR: nil}
				s.backupSettingRepo.On("GetByFilter", mock.Anything, mock.Anything).Return(bs, nil).Once()
				s.backupSettingModel.On("ModifyPitBackupSetting", mock.Anything, mock.Anything).Return(nil).Once()
			},
			wantErr: false,
		},
	}

	for name, tc := range tests {
		s.Run(name, func() {
			tc.setup()
			err := s.act.Run(ctx, s.logger, p)
			if tc.wantErr {
				s.Error(err)
				if tc.errIs != nil {
					s.Equal(tc.errIs, err)
				}
			} else {
				s.NoError(err)
			}
		})
	}
}

// --- RunAfter tests ---

func (s *enablePitrTestSuite) TestRunAfter() {
	ctx := context.Background()
	p := s.params()

	tests := map[string]struct {
		setup   func()
		wantErr bool
	}{
		"list fails": {
			setup: func() {
				s.logBackupRepo.On("List", mock.Anything, mock.Anything).
					Return(nil, bizerr.New(bizerr.ErrCodeDB, "db error")).Once()
			},
			wantErr: true,
		},
		"no running log backup yet": {
			setup: func() {
				s.logBackupRepo.On("List", mock.Anything, mock.Anything).
					Return([]entity.LogBackup{}, nil).Once()
			},
			wantErr: true,
		},
		"GetByClusterID fails after log backup found": {
			setup: func() {
				lb := makeRunningLogBackup(p.ClusterID, 99)
				s.logBackupRepo.On("List", mock.Anything, mock.Anything).
					Return([]entity.LogBackup{lb}, nil).Once()
				s.crrfSettingRepo.On("GetByClusterID", mock.Anything, p.ClusterID).
					Return(nil, bizerr.New(bizerr.ErrCodeDB, "db error")).Once()
			},
			wantErr: true,
		},
		"log backup ID already matches — idempotent skip, no save": {
			setup: func() {
				lb := makeRunningLogBackup(p.ClusterID, 99)
				crrf := &entity.CRRFSetting{LogBackupID: entity.ID(99)}
				s.logBackupRepo.On("List", mock.Anything, mock.Anything).
					Return([]entity.LogBackup{lb}, nil).Once()
				s.crrfSettingRepo.On("GetByClusterID", mock.Anything, p.ClusterID).
					Return(crrf, nil).Once()
				// No Save call expected.
			},
			wantErr: false,
		},
		"log backup ID differs — updates CRRFSetting": {
			setup: func() {
				lb := makeRunningLogBackup(p.ClusterID, 99)
				crrf := &entity.CRRFSetting{LogBackupID: entity.ID(77)} // stale / first enable
				s.logBackupRepo.On("List", mock.Anything, mock.Anything).
					Return([]entity.LogBackup{lb}, nil).Once()
				s.crrfSettingRepo.On("GetByClusterID", mock.Anything, p.ClusterID).
					Return(crrf, nil).Once()
				s.crrfSettingRepo.On("Save", mock.Anything, crrf).Return(nil).Once()
			},
			wantErr: false,
		},
		"save fails": {
			setup: func() {
				lb := makeRunningLogBackup(p.ClusterID, 99)
				crrf := &entity.CRRFSetting{LogBackupID: entity.ID(0)}
				s.logBackupRepo.On("List", mock.Anything, mock.Anything).
					Return([]entity.LogBackup{lb}, nil).Once()
				s.crrfSettingRepo.On("GetByClusterID", mock.Anything, p.ClusterID).
					Return(crrf, nil).Once()
				s.crrfSettingRepo.On("Save", mock.Anything, crrf).
					Return(bizerr.New(bizerr.ErrCodeDB, "save failed")).Once()
			},
			wantErr: true,
		},
	}

	for name, tc := range tests {
		s.Run(name, func() {
			tc.setup()
			err := s.act.RunAfter(ctx, s.logger, p)
			if tc.wantErr {
				s.Error(err)
			} else {
				s.NoError(err)
			}
		})
	}
}

func makeRunningLogBackup(clusterID uint64, id uint64) entity.LogBackup {
	lb := entity.LogBackup{Status: entity.LogBackupStatusRunning}
	lb.ID = entity.ID(id)
	lb.ClusterID = entity.ID(clusterID)
	return lb
}
