package br

import (
	"context"
	"testing"
	"time"

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

	globalngv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/entity"
	brrepo "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/br"
	resourcepoolrepo "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/resourcepool"
	mockresourcepool "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/resourcepool/mock_resourcepool"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/infra/repoimpl/schema"
)

type passthroughPauseFenceTransactioner struct{}

func (passthroughPauseFenceTransactioner) Transaction(ctx context.Context, callback func(context.Context) error) error {
	return callback(ctx)
}

func TestCheckSharedPoolPauseWindow(t *testing.T) {
	tests := []struct {
		name       string
		state      globalngv1.ResourcePool_LifecycleState
		deleted    bool
		wantPaused bool
	}{
		{name: "active", state: globalngv1.ResourcePool_ACTIVE},
		{name: "modifying", state: globalngv1.ResourcePool_MODIFYING},
		{name: "pause prechecking", state: globalngv1.ResourcePool_PAUSE_PRECHECKING, wantPaused: true},
		{name: "pausing", state: globalngv1.ResourcePool_PAUSING, wantPaused: true},
		{name: "paused", state: globalngv1.ResourcePool_PAUSED, wantPaused: true},
		{name: "resuming", state: globalngv1.ResourcePool_RESUMING, wantPaused: true},
		{name: "deleted paused pool", state: globalngv1.ResourcePool_PAUSED, deleted: true},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctrl := gomock.NewController(t)
			repo := mockresourcepool.NewMockRepo(ctrl)
			pool := &globalngv1.ResourcePool{Id: 42, LifecycleState: tt.state}
			if tt.deleted {
				pool.DeleteTime = timestamppb.Now()
			}
			repo.EXPECT().Load(gomock.Any(), uint64(42), true).Return(&entity.ResourcePool{
				ResourcePool: pool,
			}, nil)

			paused, err := checkSharedPoolPauseWindow(t.Context(), 42, repo)
			require.NoError(t, err)
			require.Equal(t, tt.wantPaused, paused)
		})
	}
}

func TestRunWithSharedPoolPauseFence(t *testing.T) {
	tests := []struct {
		name         string
		state        globalngv1.ResourcePool_LifecycleState
		deleted      bool
		wantSkipped  bool
		wantMutation bool
	}{
		{name: "active executes mutation", state: globalngv1.ResourcePool_ACTIVE, wantMutation: true},
		{name: "pause prechecking skips mutation", state: globalngv1.ResourcePool_PAUSE_PRECHECKING, wantSkipped: true},
		{name: "pausing skips mutation", state: globalngv1.ResourcePool_PAUSING, wantSkipped: true},
		{name: "paused skips mutation", state: globalngv1.ResourcePool_PAUSED, wantSkipped: true},
		{name: "resuming skips mutation", state: globalngv1.ResourcePool_RESUMING, wantSkipped: true},
		{name: "deleted paused pool executes cleanup", state: globalngv1.ResourcePool_PAUSED, deleted: true, wantMutation: true},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctrl := gomock.NewController(t)
			repo := mockresourcepool.NewMockRepo(ctrl)
			repo.EXPECT().LoadLifecycleForUpdate(gomock.Any(), uint64(42), true).Return(&resourcepoolrepo.Lifecycle{
				State:     tt.state,
				IsDeleted: tt.deleted,
			}, nil)

			mutationCalled := false
			skipped, err := runWithSharedPoolPauseFence(
				t.Context(),
				42,
				passthroughPauseFenceTransactioner{},
				repo,
				func(context.Context) error {
					mutationCalled = true
					return nil
				},
			)

			require.NoError(t, err)
			require.Equal(t, tt.wantSkipped, skipped)
			require.Equal(t, tt.wantMutation, mutationCalled)
		})
	}
}

func TestExcludeBackupTimePointsOnlyRemovesPauseIntervals(t *testing.T) {
	start := time.Date(2026, 8, 15, 1, 0, 0, 0, time.UTC)
	end := start.Add(2 * time.Hour)
	points := []time.Time{start.Add(-time.Hour), start, start.Add(time.Hour), end, end.Add(time.Hour)}

	actual := excludeBackupTimePoints(points, []brrepo.BackupReconciliationExclusion{{StartAt: start, EndAt: &end}})

	require.Equal(t, []time.Time{points[0], points[4]}, actual)
	require.Empty(t, calculateMissingTimePoints(nil, points))
}

func TestNormalBackupSyncDoesNotCallTiBRDuringPause(t *testing.T) {
	slots := &backupSyncSlots{
		pauseChecker: func(context.Context, uint64) (bool, error) { return true, nil },
	}

	// No downstream repositories or Regional client are installed. Reaching any
	// of them would panic and prove that a paused pool was not skipped.
	slots.syncOneUnit(t.Context(), &toBeSyncedUnit{
		slotTime: time.Now().UTC(),
		sharedPoolClusters: map[uint64]map[uint64]bool{
			42: {100: true},
		},
	})
}

func TestSyntheticFailedBackupIsFencedAfterPauseAdmission(t *testing.T) {
	backupTime := time.Now().UTC()
	slots := &backupSyncSlots{
		pauseFencer: func(context.Context, uint64, func(context.Context) error) (bool, error) {
			return true, nil
		},
		backupSettings: map[uint64]*entity.BackupSetting{
			100: {BackupSetting: &schema.BackupSetting{ClusterId: 100}},
		},
	}
	record := &entity.Backup{Backup: &schema.Backup{
		ClusterId:    100,
		SharedPoolId: 42,
		BackupTime:   backupTime,
		State:        schema.BackupStateFailed,
	}}

	require.Empty(t, slots.saveBackupSyncResult(t.Context(), map[uint64]*entity.Backup{100: record}))
	require.Nil(t, slots.backupSettings[100].LastAutoBackupAt)
}
