package br

import (
	"testing"

	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	dplanemgrv1 "github.com/tidbcloud/cluster-service-ng/api/dataplane-manager/v1"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/pkg/storage/mysql/schema"
)

func TestMakeLifecycleRulesSPCreating(t *testing.T) {
	sharedPool := newTestSharedPool()
	rules := MakeLifecycleRules(newBackupGcSetting(
		schema.BackupGcStatusSPCreating,
		uint32Ptr(2),
		uint32Ptr(3),
		uint32Ptr(4),
		uint32Ptr(5),
		uint32Ptr(6),
	), sharedPool)

	dfsPrefix := sharedPool.GetDfsPrefix()

	assertRuleCount(t, rules, 7)

	assertAbortIncompleteMultipartRule(t, rules)

	assertLifecycleRule(t, rules, RuleIdExpireTiflashData, func(rule *dplanemgrv1.LifecycleRule) {
		expectEqual(t, rule.GetPrefixFilter(), sharedPool.GetTiFlashRootPrefix()+"/")
		if len(rule.GetTagsFilter()) != 1 {
			t.Fatalf("expected one tag filter, got %d", len(rule.GetTagsFilter()))
		}
		tag := rule.GetTagsFilter()[0]
		expectEqual(t, tag.GetKey(), "tiflash_deleted")
		expectEqual(t, tag.GetValue(), "true")
		expectEqual(t, rule.GetExpirationDays(), uint32(1))
	})

	assertLifecycleRule(t, rules, RuleIdPackedBackupExpirationRule, func(rule *dplanemgrv1.LifecycleRule) {
		expectEqual(t, rule.GetPrefixFilter(), dfsPrefix+"/packed_backup/")
		expectEqual(t, rule.GetExpirationDays(), uint32(2))
	})

	assertLifecycleRule(t, rules, RuleIdBackupMetaExpirationRule, func(rule *dplanemgrv1.LifecycleRule) {
		expectEqual(t, rule.GetPrefixFilter(), dfsPrefix+"/backup/")
		expectEqual(t, rule.GetExpirationDays(), uint32(3))
	})

	assertLifecycleRule(t, rules, RuleIdLightweightWALAndRlogExpirationRule, func(rule *dplanemgrv1.LifecycleRule) {
		expectEqual(t, rule.GetPrefixFilter(), dfsPrefix+"/store_backup/")
		expectEqual(t, rule.GetExpirationDays(), uint32(4))
	})

	assertLifecycleRule(t, rules, RuleIdUnusedSSTExpirationRule, func(rule *dplanemgrv1.LifecycleRule) {
		expectEqual(t, rule.GetPrefixFilter(), dfsPrefix+"/")
		if len(rule.GetTagsFilter()) != 1 {
			t.Fatalf("expected one tag filter, got %d", len(rule.GetTagsFilter()))
		}
		tag := rule.GetTagsFilter()[0]
		expectEqual(t, tag.GetKey(), "deleted")
		expectEqual(t, tag.GetValue(), "true")
		expectEqual(t, rule.GetExpirationDays(), uint32(5))
	})

	assertLifecycleRule(t, rules, RuleIdCleanupArchiveRule, func(rule *dplanemgrv1.LifecycleRule) {
		expectEqual(t, rule.GetPrefixFilter(), dfsPrefix+"/archive")
		expectEqual(t, rule.GetExpirationDays(), uint32(6))
	})
}

func TestMakeLifecycleRulesSPDeleting(t *testing.T) {
	sharedPool := newTestSharedPool()
	backupSetting := newBackupGcSetting(
		schema.BackupGcStatusSPDeleting,
		uint32Ptr(2),
		uint32Ptr(3),
		uint32Ptr(4),
		uint32Ptr(5),
		uint32Ptr(6),
	)

	rules := MakeLifecycleRules(backupSetting, sharedPool)

	assertRuleCount(t, rules, 9)

	assertLifecycleRule(t, rules, RuleIdCleanupTiflashData, func(rule *dplanemgrv1.LifecycleRule) {
		expectEqual(t, rule.GetPrefixFilter(), sharedPool.GetTiFlashRootPrefix()+"/")
		expectEqual(t, rule.GetExpirationDays(), uint32(1))
	})

	assertLifecycleRule(t, rules, RuleIdCleanupTiciData, func(rule *dplanemgrv1.LifecycleRule) {
		expectEqual(t, rule.GetPrefixFilter(), sharedPool.GetTiCIPrefix()+"/")
		expectEqual(t, rule.GetExpirationDays(), uint32(1))
	})

	// ensure all base rules still present
	assertLifecycleRuleExists(t, rules, RuleIdCleanupArchiveRule)
	assertLifecycleRuleExists(t, rules, RuleIdUnusedSSTExpirationRule)
	assertLifecycleRuleExists(t, rules, RuleIdLightweightWALAndRlogExpirationRule)
	assertLifecycleRuleExists(t, rules, RuleIdBackupMetaExpirationRule)
	assertLifecycleRuleExists(t, rules, RuleIdPackedBackupExpirationRule)
	assertLifecycleRuleExists(t, rules, RuleIdAbortIncompleteMultipartUpload)
	assertLifecycleRuleExists(t, rules, RuleIdExpireTiflashData)
}

func TestMakeLifecycleRulesAutoBackupExpired(t *testing.T) {
	sharedPool := newTestSharedPool()
	backupSetting := newBackupGcSetting(
		schema.BackupGcStatusAutoBackupExpired,
		uint32Ptr(2),
		uint32Ptr(3),
		uint32Ptr(4),
		uint32Ptr(5),
		uint32Ptr(6),
	)

	rules := MakeLifecycleRules(backupSetting, sharedPool)

	assertRuleCount(t, rules, 5)

	assertLifecycleRuleExists(t, rules, RuleIdAbortIncompleteMultipartUpload)
	assertLifecycleRuleExists(t, rules, RuleIdExpireTiflashData)
	assertLifecycleRule(t, rules, RuleIdCleanupTiflashData, func(rule *dplanemgrv1.LifecycleRule) {
		expectEqual(t, rule.GetPrefixFilter(), sharedPool.GetTiFlashRootPrefix()+"/")
		expectEqual(t, rule.GetExpirationDays(), uint32(1))
	})
	assertLifecycleRule(t, rules, RuleIdCleanupAllAutoBackupData, func(rule *dplanemgrv1.LifecycleRule) {
		expectEqual(t, rule.GetPrefixFilter(), sharedPool.GetDfsPrefix()+"/")
		expectEqual(t, rule.GetExpirationDays(), uint32(1))
	})
	assertLifecycleRule(t, rules, RuleIdCleanupTiciData, func(rule *dplanemgrv1.LifecycleRule) {
		expectEqual(t, rule.GetPrefixFilter(), sharedPool.GetTiCIPrefix()+"/")
		expectEqual(t, rule.GetExpirationDays(), uint32(1))
	})

	assertLifecycleRuleMissing(t, rules, RuleIdBackupMetaExpirationRule)
	assertLifecycleRuleMissing(t, rules, RuleIdLightweightWALAndRlogExpirationRule)
	assertLifecycleRuleMissing(t, rules, RuleIdUnusedSSTExpirationRule)
	assertLifecycleRuleMissing(t, rules, RuleIdCleanupArchiveRule)
	assertLifecycleRuleMissing(t, rules, RuleIdPackedBackupExpirationRule)
}

func TestMakeLifecycleRulesSkipsZeroValues(t *testing.T) {
	sharedPool := newTestSharedPool()
	rules := MakeLifecycleRules(newBackupGcSetting(
		schema.BackupGcStatusSPRunning,
		uint32Ptr(0),
		uint32Ptr(3),
		uint32Ptr(0),
		uint32Ptr(0),
		nil,
	), sharedPool)

	assertLifecycleRuleExists(t, rules, RuleIdBackupMetaExpirationRule)
	assertLifecycleRuleMissing(t, rules, RuleIdPackedBackupExpirationRule)
	assertLifecycleRuleMissing(t, rules, RuleIdLightweightWALAndRlogExpirationRule)
	assertLifecycleRuleMissing(t, rules, RuleIdUnusedSSTExpirationRule)
	assertLifecycleRuleMissing(t, rules, RuleIdCleanupArchiveRule)
}

func TestMakeLifecycleRulesAppliesArchiveSafetyFloor(t *testing.T) {
	sharedPool := newTestSharedPool()
	setting := newBackupGcSetting(
		schema.BackupGcStatusSPRunning,
		uint32Ptr(2), uint32Ptr(2), uint32Ptr(3), uint32Ptr(7), uint32Ptr(2),
	)
	setting.MinT3DataAgeDays = 3
	rules := MakeLifecycleRules(setting, sharedPool)

	for _, id := range []string{
		RuleIdBackupMetaExpirationRule,
		RuleIdLightweightWALAndRlogExpirationRule,
	} {
		assertLifecycleRule(t, rules, id, func(rule *dplanemgrv1.LifecycleRule) {
			expectEqual(t, rule.GetExpirationDays(), uint32(4))
		})
	}
	assertLifecycleRule(t, rules, RuleIdUnusedSSTExpirationRule, func(rule *dplanemgrv1.LifecycleRule) {
		expectEqual(t, rule.GetExpirationDays(), uint32(7))
	})
	assertLifecycleRule(t, rules, RuleIdPackedBackupExpirationRule, func(rule *dplanemgrv1.LifecycleRule) {
		expectEqual(t, rule.GetExpirationDays(), uint32(2))
	})
	assertLifecycleRule(t, rules, RuleIdCleanupArchiveRule, func(rule *dplanemgrv1.LifecycleRule) {
		expectEqual(t, rule.GetExpirationDays(), uint32(2))
	})

	// The Regional floor only affects rendered physical rules. The setting
	// persisted from Global keeps its original logical values.
	expectEqual(t, *setting.MaxT1MetaLifetimeDays, uint32(2))
	expectEqual(t, *setting.MaxT1WalLifetimeDays, uint32(3))
	expectEqual(t, *setting.MaxT1PackedLifetimeDays, uint32(2))
}

func TestArchiveSafeBackupGcSettingForTransitionDelay(t *testing.T) {
	setting := newBackupGcSetting(
		schema.BackupGcStatusSPRunning,
		uint32Ptr(4), uint32Ptr(4), uint32Ptr(4), uint32Ptr(7), uint32Ptr(30),
	)
	setting.MinT3DataAgeDays = 3

	effective := ArchiveSafeBackupGcSettingForDelay(setting, 5)
	expectEqual(t, *effective.MaxT1MetaLifetimeDays, uint32(6))
	expectEqual(t, *effective.MaxT1WalLifetimeDays, uint32(6))
	expectEqual(t, *effective.MaxT2SstLifetimeDays, uint32(7))
	// Packed restore artifacts are independent of T3 archive progression.
	expectEqual(t, *effective.MaxT1PackedLifetimeDays, uint32(4))
	// The desired setting remains unchanged for the final post-update apply.
	expectEqual(t, *setting.MaxT1MetaLifetimeDays, uint32(4))
}

func TestMakeLifecycleRulesDisablesBRObjectExpirationDuringPause(t *testing.T) {
	sharedPool := newTestSharedPool()
	sharedPool.PauseResumeState = entity.PauseResumeStatePaused
	rules := MakeLifecycleRules(newBackupGcSetting(
		schema.BackupGcStatusSPRunning,
		uint32Ptr(2), uint32Ptr(3), uint32Ptr(4), uint32Ptr(5), uint32Ptr(6),
	), sharedPool)

	for _, id := range []string{
		RuleIdBackupMetaExpirationRule,
		RuleIdLightweightWALAndRlogExpirationRule,
		RuleIdUnusedSSTExpirationRule,
		RuleIdCleanupArchiveRule,
		RuleIdPackedBackupExpirationRule,
	} {
		assertLifecycleRule(t, rules, id, func(rule *dplanemgrv1.LifecycleRule) {
			if rule.GetEnabled() {
				t.Fatalf("expected BR rule %q to be disabled during pause", id)
			}
		})
	}

	assertLifecycleRule(t, rules, RuleIdAbortIncompleteMultipartUpload, func(rule *dplanemgrv1.LifecycleRule) {
		if !rule.GetEnabled() {
			t.Fatal("multipart upload expiration must remain enabled during pause")
		}
	})
	assertLifecycleRule(t, rules, RuleIdExpireTiflashData, func(rule *dplanemgrv1.LifecycleRule) {
		if !rule.GetEnabled() {
			t.Fatal("TiFlash expiration must remain enabled during pause")
		}
	})
}

func TestMakeLifecycleRulesDisablesCleanupAllAutoBackupDataDuringPause(t *testing.T) {
	sharedPool := newTestSharedPool()
	sharedPool.PauseResumeState = entity.PauseResumeStateResuming
	rules := MakeLifecycleRules(newBackupGcSetting(
		schema.BackupGcStatusAutoBackupExpired,
		uint32Ptr(2), uint32Ptr(3), uint32Ptr(4), uint32Ptr(5), uint32Ptr(6),
	), sharedPool)

	assertLifecycleRule(t, rules, RuleIdCleanupAllAutoBackupData, func(rule *dplanemgrv1.LifecycleRule) {
		if rule.GetEnabled() {
			t.Fatal("all-auto-backup cleanup must be disabled during resume protection")
		}
	})
}

func TestMakeLifecycleRulesWithoutPauseProtectionRestoresBRExpiration(t *testing.T) {
	sharedPool := newTestSharedPool()
	sharedPool.PauseResumeState = entity.PauseResumeStateResuming
	rules := MakeLifecycleRulesWithoutPauseProtection(newBackupGcSetting(
		schema.BackupGcStatusSPRunning,
		uint32Ptr(2), uint32Ptr(3), uint32Ptr(4), uint32Ptr(5), uint32Ptr(6),
	), sharedPool)

	for _, id := range []string{
		RuleIdBackupMetaExpirationRule,
		RuleIdLightweightWALAndRlogExpirationRule,
		RuleIdUnusedSSTExpirationRule,
		RuleIdCleanupArchiveRule,
		RuleIdPackedBackupExpirationRule,
	} {
		assertLifecycleRule(t, rules, id, func(rule *dplanemgrv1.LifecycleRule) {
			if !rule.GetEnabled() {
				t.Fatalf("expected BR rule %q to be restored after archive gate", id)
			}
		})
	}
}

func newTestSharedPool() *entity.SharedPool {
	return &entity.SharedPool{
		SharedPool: apiv1.SharedPool{
			ObjectStorage: &apiv1.SharedPool_ObjectStorage{},
		},
	}
}

func newBackupGcSetting(status string, packed, meta, wal, sst, archive *uint32) *entity.BackupGcSetting {
	setting := &entity.BackupGcSetting{}
	setting.Status = status
	setting.MaxT1PackedLifetimeDays = packed
	setting.MaxT1MetaLifetimeDays = meta
	setting.MaxT1WalLifetimeDays = wal
	setting.MaxT2SstLifetimeDays = sst
	setting.MaxT3DataLifetimeDays = archive
	return setting
}

func uint32Ptr(v uint32) *uint32 {
	return &v
}

func findLifecycleRule(rules []*dplanemgrv1.LifecycleRule, id string) *dplanemgrv1.LifecycleRule {
	for _, rule := range rules {
		if rule != nil && rule.GetId() == id {
			return rule
		}
	}
	return nil
}

func assertLifecycleRuleExists(t *testing.T, rules []*dplanemgrv1.LifecycleRule, id string) {
	t.Helper()
	if findLifecycleRule(rules, id) == nil {
		t.Fatalf("expected lifecycle rule %q to exist", id)
	}
}

func assertLifecycleRuleMissing(t *testing.T, rules []*dplanemgrv1.LifecycleRule, id string) {
	t.Helper()
	if findLifecycleRule(rules, id) != nil {
		t.Fatalf("expected lifecycle rule %q to be absent", id)
	}
}

func assertLifecycleRule(t *testing.T, rules []*dplanemgrv1.LifecycleRule, id string, validate func(*dplanemgrv1.LifecycleRule)) {
	t.Helper()
	rule := findLifecycleRule(rules, id)
	if rule == nil {
		t.Fatalf("expected lifecycle rule %q to exist", id)
	}
	if validate != nil {
		validate(rule)
	}
}

func assertAbortIncompleteMultipartRule(t *testing.T, rules []*dplanemgrv1.LifecycleRule) {
	t.Helper()
	assertLifecycleRule(t, rules, RuleIdAbortIncompleteMultipartUpload, func(rule *dplanemgrv1.LifecycleRule) {
		expectEqual(t, rule.GetPrefixFilter(), "")
		expectEqual(t, rule.GetIncompleteUploadExpirationDays(), uint32(1))
		expectEqual(t, rule.GetEnabled(), true)
	})
}

func assertRuleCount(t *testing.T, rules []*dplanemgrv1.LifecycleRule, expected int) {
	t.Helper()
	if len(rules) != expected {
		t.Fatalf("expected %d lifecycle rules, got %d", expected, len(rules))
	}
}

func expectEqual[T comparable](t *testing.T, got, want T) {
	t.Helper()
	if got != want {
		t.Fatalf("expected %v, got %v", want, got)
	}
}
