package compact_test

import (
	"context"
	"fmt"
	"strconv"
	"testing"
	"time"

	"github.com/AlekSi/pointer"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	"github.com/stretchr/testify/suite"
	feature_gate_mock "github.com/tidbcloud/dataflow-service/app/internal/feature_gate/mock"
	"github.com/tidbcloud/dataflow-service/app/models/backup/impl"
	"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_mock "github.com/tidbcloud/dataflow-service/app/repo/mock"
	"github.com/tidbcloud/dataflow-service/app/utils"
	service_config "github.com/tidbcloud/dataflow-service/internal/config"
	grpcli_mock "github.com/tidbcloud/dataflow-service/pkg/grpc/grpcli/mock"
	infra_mock "github.com/tidbcloud/dataflow-service/pkg/infra-api/mock"
	"github.com/tidbcloud/pkgv2/db"
	"gorm.io/gorm"
)

// Helper functions for dynamic TSO generation
func getCurrentTestCheckpointTSO() string {
	return fmt.Sprintf("%d", utils.GoTimeToTSO(time.Now()))
}

type compactBackupSchedulerUnitTestSuite struct {
	suite.Suite

	scheduler *impl.CompactBackupSchedulerImpl

	mockLogBackupRepo     *repo_mock.LogBackupRepo
	mockCompactBackupRepo *repo_mock.CompactBackupRepo
	mockCompactFacade     *infra_mock.CompactFacade
	mockMgmtGrpcClient    *grpcli_mock.MgmtSvcGrpcClient
	mockFeatureGate       *feature_gate_mock.FeatureGate

	db            *gorm.DB
	transProvider db.TransProvider
}

func TestCompactBackupSchedulerUnitTestSuite(t *testing.T) {
	suite.Run(t, new(compactBackupSchedulerUnitTestSuite))
}

func (s *compactBackupSchedulerUnitTestSuite) SetupSuite() {
	// Initialize global config
	globalConfig := service_config.Global()
	globalConfig.CompactConfig.CompactBackupCycle = "5h50m"

	// Setup database
	testDB, err := repo.GetFakeDB()
	require.NoError(s.T(), err)
	s.db = testDB
	s.transProvider = db.NewProvider(testDB)
}

func (s *compactBackupSchedulerUnitTestSuite) SetupTest() {
	// Create fresh mocks for each test
	s.mockLogBackupRepo = repo_mock.NewLogBackupRepo(s.T())
	s.mockCompactBackupRepo = repo_mock.NewCompactBackupRepo(s.T())
	s.mockCompactFacade = infra_mock.NewCompactFacade(s.T())
	s.mockMgmtGrpcClient = grpcli_mock.NewMgmtSvcGrpcClient(s.T())
	s.mockFeatureGate = feature_gate_mock.NewFeatureGate(s.T())

	// Create scheduler instance
	s.scheduler = impl.NewCompactBackupScheduler(
		s.transProvider,
		s.mockLogBackupRepo,
		s.mockCompactBackupRepo,
		nil, // CompactBackupModel not needed for unit tests
		s.mockCompactFacade,
		s.mockMgmtGrpcClient,
		s.mockFeatureGate,
	).(*impl.CompactBackupSchedulerImpl)
}

// Unit test for ShouldCompactLogBackup method
func (s *compactBackupSchedulerUnitTestSuite) TestShouldCompactLogBackup() {
	ctx := context.Background()

	testCases := []struct {
		name                string
		logBackup           *entity.LogBackup
		mockLastSuccessTask *entity.CompactBackup
		expected            bool
		description         string
	}{
		{
			name: "should_not_compact_status_paused",
			logBackup: &entity.LogBackup{
				Status:       entity.LogBackupStatusPaused,
				CheckpointTS: getCurrentTestCheckpointTSO(),
				StartTS:      pointer.ToString(s.timeToTSOString(time.Now().Add(-8 * time.Hour))),
			},
			expected:    false,
			description: "LogBackup is Paused, should not compact",
		},
		{
			name: "should_not_compact_empty_checkpoint",
			logBackup: &entity.LogBackup{
				Status:       entity.LogBackupStatusRunning,
				CheckpointTS: "",
				StartTS:      pointer.ToString(s.timeToTSOString(time.Now().Add(-8 * time.Hour))),
			},
			expected:    false,
			description: "CheckpointTS is empty, should not compact",
		},
		{
			name: "should_not_compact_runtime_insufficient",
			logBackup: &entity.LogBackup{
				Status:       entity.LogBackupStatusRunning,
				CheckpointTS: getCurrentTestCheckpointTSO(),
				StartTS:      pointer.ToString(s.timeToTSOString(time.Now().Add(-2 * time.Hour))),
			},
			expected:    false,
			description: "Runtime insufficient (2h < 5h50m), should not compact",
		},
		{
			name: "should_not_compact_cycle_not_reached",
			logBackup: &entity.LogBackup{
				Status:       entity.LogBackupStatusRunning,
				CheckpointTS: getCurrentTestCheckpointTSO(),
				StartTS:      pointer.ToString(s.timeToTSOString(time.Now().Add(-8 * time.Hour))),
			},
			mockLastSuccessTask: &entity.CompactBackup{
				Updatable: entity.Updatable{
					Base: entity.Base{
						CreatedAt: time.Now().Add(-1 * time.Hour), // 1 hour ago < 5h50m
					},
				},
			},
			expected:    false,
			description: "Last success 2h ago, cycle not reached, should not compact",
		},
		{
			name: "should_compact_conditions_met_no_previous_task",
			logBackup: &entity.LogBackup{
				Status:       entity.LogBackupStatusRunning,
				CheckpointTS: getCurrentTestCheckpointTSO(),
				StartTS:      pointer.ToString(s.timeToTSOString(time.Now().Add(-8 * time.Hour))),
			},
			mockLastSuccessTask: nil, // No previous tasks
			expected:            true,
			description:         "Runtime sufficient, no previous tasks, should compact",
		},
		{
			name: "should_compact_cycle_reached",
			logBackup: &entity.LogBackup{
				Status:       entity.LogBackupStatusRunning,
				CheckpointTS: getCurrentTestCheckpointTSO(),
				StartTS:      pointer.ToString(s.timeToTSOString(time.Now().Add(-8 * time.Hour))),
			},
			mockLastSuccessTask: &entity.CompactBackup{
				Updatable: entity.Updatable{
					Base: entity.Base{
						CreatedAt: time.Now().Add(-12 * time.Hour), // 12 hours ago > 5h50m
					},
				},
			},
			expected:    true,
			description: "Runtime sufficient, cycle reached, should compact",
		},
	}

	for i, tc := range testCases {
		s.Run(tc.name, func() {
			// Use unique LogBackupID for each test case to avoid mock conflicts
			logBackupID := uint64(100 + i)
			tc.logBackup.Updatable.Base.ID = entity.ID(logBackupID)

			// Only mock getLastSuccessCompactTask for cases that will actually call it
			// The method only calls getLastSuccessCompactTask if:
			// 1. Status is Running (checked)
			// 2. CheckpointTS is not empty (checked)
			// 3. Runtime is enough (needs to check if StartTS indicates >= 5h50m runtime)
			shouldMockLastSuccessTask := tc.logBackup.Status == entity.LogBackupStatusRunning &&
				tc.logBackup.CheckpointTS != "" &&
				tc.logBackup.StartTS != nil && *tc.logBackup.StartTS != ""

			// Check if this test case has sufficient runtime (>= 5h50m)
			if shouldMockLastSuccessTask && tc.logBackup.StartTS != nil {
				// Parse the TSO to check if runtime >= 5h50m
				startTSO, err := strconv.ParseUint(*tc.logBackup.StartTS, 10, 64)
				if err == nil {
					startTime := utils.GetTimeFromTS(startTSO)
					runTime := time.Since(startTime)
					compactCycle := 5*time.Hour + 50*time.Minute
					if runTime < compactCycle {
						shouldMockLastSuccessTask = false // Runtime insufficient, won't call getLastSuccessCompactTask
					}
				}
			}

			if shouldMockLastSuccessTask {
				var mockResult []*entity.CompactBackup
				if tc.mockLastSuccessTask != nil {
					mockResult = []*entity.CompactBackup{tc.mockLastSuccessTask}
				} else {
					mockResult = []*entity.CompactBackup{}
				}

				s.mockCompactBackupRepo.On("ListByFilter", mock.Anything, mock.MatchedBy(func(filter *backup_repo.CompactBackupFilter) bool {
					return *filter.LogBackupID == logBackupID && *filter.Status == entity.CompactBackupStatusSuccess
				})).Return(mockResult, nil).Once()
			}

			// Mock GetAnyClusterByID for shouldCompactLogBackup (needed when checking runtime requirements)
			if tc.expected || tc.name == "should_not_compact_runtime_insufficient" {
				s.mockMgmtGrpcClient.On("GetAnyDedicatedClusterBasicByID", mock.Anything, tc.logBackup.ClusterID.Uint64()).
					Return(createValidCluster(), nil).Maybe()
			}

			// Call the method directly
			result, err := s.scheduler.ShouldCompactLogBackup(ctx, tc.logBackup)

			// Verify results
			s.NoError(err, "ShouldCompactLogBackup should not return error for test case: %s", tc.name)
			s.Equal(tc.expected, result, "ShouldCompactLogBackup result mismatch for test case: %s", tc.name)
		})
	}
}

// Unit test for CheckLogBackupRunTimeEnough method
func (s *compactBackupSchedulerUnitTestSuite) TestCheckLogBackupRunTimeEnough() {
	ctx := context.Background()
	compactCycle := 5*time.Hour + 50*time.Minute

	testCases := []struct {
		name        string
		hoursAgo    int
		expected    bool
		description string
	}{
		{
			name:        "insufficient_runtime_2h",
			hoursAgo:    2,
			expected:    false,
			description: "2 hours runtime < 5h50m requirement",
		},
		{
			name:        "sufficient_runtime_8h",
			hoursAgo:    8,
			expected:    true,
			description: "8 hours runtime > 5h50m requirement",
		},
		{
			name:        "edge_case_6h",
			hoursAgo:    6,
			expected:    true,
			description: "6 hours runtime > 5h50m requirement",
		},
	}

	for _, tc := range testCases {
		s.Run(tc.name, func() {
			logBackup := &entity.LogBackup{
				StartTS: pointer.ToString(s.timeToTSOString(time.Now().Add(-time.Duration(tc.hoursAgo) * time.Hour))),
			}

			result := s.scheduler.CheckLogBackupRunTimeEnough(ctx, logBackup, compactCycle)
			s.Equal(tc.expected, result, "CheckLogBackupRunTimeEnough result mismatch for test case: %s", tc.name)
		})
	}
}

// Unit test for CheckWhetherReachCompactCycle method
func (s *compactBackupSchedulerUnitTestSuite) TestCheckWhetherReachCompactCycle() {
	ctx := context.Background()
	compactCycle := 5*time.Hour + 50*time.Minute

	testCases := []struct {
		name                string
		lastSuccessHoursAgo *int // nil means no previous task
		expected            bool
		description         string
	}{
		{
			name:                "no_previous_task",
			lastSuccessHoursAgo: nil,
			expected:            true,
			description:         "No previous successful task, should compact",
		},
		{
			name:                "cycle_not_reached_3h",
			lastSuccessHoursAgo: pointer.ToInt(3),
			expected:            false,
			description:         "Last success 3h ago < 5h50m cycle",
		},
		{
			name:                "cycle_reached_12h",
			lastSuccessHoursAgo: pointer.ToInt(12),
			expected:            true,
			description:         "Last success 12h ago > 5h50m cycle",
		},
		{
			name:                "edge_case_6h",
			lastSuccessHoursAgo: pointer.ToInt(6),
			expected:            true,
			description:         "Last success 6h ago > 5h50m cycle",
		},
	}

	for _, tc := range testCases {
		s.Run(tc.name, func() {
			logBackup := &entity.LogBackup{
				Updatable: entity.Updatable{
					Base: entity.Base{ID: entity.ID(200)},
				},
			}

			// Mock getLastSuccessCompactTask
			var mockResult []*entity.CompactBackup
			if tc.lastSuccessHoursAgo != nil {
				mockTask := &entity.CompactBackup{
					Updatable: entity.Updatable{
						Base: entity.Base{
							CreatedAt: time.Now().Add(-time.Duration(*tc.lastSuccessHoursAgo) * time.Hour),
						},
					},
				}
				mockResult = []*entity.CompactBackup{mockTask}
			} else {
				mockResult = []*entity.CompactBackup{}
			}

			s.mockCompactBackupRepo.On("ListByFilter", mock.Anything, mock.MatchedBy(func(filter *backup_repo.CompactBackupFilter) bool {
				return *filter.LogBackupID == 200 && *filter.Status == entity.CompactBackupStatusSuccess
			})).Return(mockResult, nil).Once()

			result := s.scheduler.CheckWhetherReachCompactCycle(ctx, logBackup, compactCycle)
			s.Equal(tc.expected, result, "CheckWhetherReachCompactCycle result mismatch for test case: %s", tc.name)
		})
	}
}

// Helper method for time conversion
func (s *compactBackupSchedulerUnitTestSuite) timeToTSOString(t time.Time) string {
	tso := utils.GoTimeToTSO(t)
	return fmt.Sprintf("%d", tso)
}
