package service

import (
	"encoding/base64"
	"fmt"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/serverless-service/internal/domain/constant"
	"github.com/tidbcloud/serverless-service/internal/domain/entity"
	"github.com/tidbcloud/serverless-service/internal/domain/vo"
)

func TestVirtualBackupIDRoundTrip(t *testing.T) {
	scheduleTime := time.Date(2026, 7, 8, 10, 0, 0, 0, time.UTC)

	backupID := generateVirtualBackupID("12345", scheduleTime)
	ref, ok, malformed := parseVirtualBackupID(backupID)

	require.True(t, ok)
	require.False(t, malformed)
	require.Equal(t, "12345", ref.clusterID)
	require.True(t, scheduleTime.Equal(ref.scheduleTime))
}

func TestParseVirtualBackupIDRejectsNonCanonicalUnix(t *testing.T) {
	scheduleTime := time.Date(2026, 7, 8, 10, 0, 0, 0, time.UTC)
	for _, unix := range []string{
		fmt.Sprintf("+%d", scheduleTime.Unix()),
		fmt.Sprintf("0%d", scheduleTime.Unix()),
	} {
		payload := fmt.Sprintf("12345:%s", unix)
		backupID := virtualBackupIDPrefix + base64.RawURLEncoding.EncodeToString([]byte(payload))

		ref, ok, malformed := parseVirtualBackupID(backupID)

		require.Nil(t, ref)
		require.False(t, ok)
		require.True(t, malformed)
	}
}

func TestParseVirtualBackupIDRejectsInvalidClusterID(t *testing.T) {
	scheduleTime := time.Date(2026, 7, 8, 10, 0, 0, 0, time.UTC)
	for _, clusterID := range []string{
		"",
		"+12345",
		"012345",
		"not-a-cluster",
		"18446744073709551616",
	} {
		payload := fmt.Sprintf("%s:%d", clusterID, scheduleTime.Unix())
		backupID := virtualBackupIDPrefix + base64.RawURLEncoding.EncodeToString([]byte(payload))

		ref, ok, malformed := parseVirtualBackupID(backupID)

		require.Nil(t, ref)
		require.False(t, ok)
		require.True(t, malformed)
	}
}

func TestEnumerateScheduleTimes(t *testing.T) {
	start := time.Date(2026, 7, 8, 0, 0, 0, 0, time.UTC)
	end := time.Date(2026, 7, 10, 0, 0, 0, 0, time.UTC)

	times, err := enumerateScheduleTimes("0 0 10 * * *", start, end)

	require.NoError(t, err)
	require.Equal(t, []time.Time{
		time.Date(2026, 7, 8, 10, 0, 0, 0, time.UTC),
		time.Date(2026, 7, 9, 10, 0, 0, 0, time.UTC),
	}, times)
}

func TestBuildVirtualBackupsExcludesHistoryEndBoundary(t *testing.T) {
	scheduleTime := time.Date(2026, 7, 8, 10, 0, 0, 0, time.UTC)
	validFrom := scheduleTime.Add(-time.Hour)
	history := &entity.BackupScheduleHistory{
		ClusterID:    "12345",
		ScheduleCron: "0 0 10 * * *",
		Retention:    24,
		Kubernetes:   "test-kubernetes",
		ValidFrom:    &validFrom,
		ValidTo:      &scheduleTime,
	}

	backups := buildVirtualBackups(
		t.Context(),
		[]*entity.BackupScheduleHistory{history},
		scheduleTime,
		noCutover,
		nil,
		0,
	)

	require.Empty(t, backups)
}

func TestSuppressPersistedBackupsWithTombstonesUsesRestorePointKey(t *testing.T) {
	scheduleTime := time.Date(2026, 7, 8, 10, 0, 0, 0, time.UTC)
	deleted := &entity.UserBackup{ID: "legacy-late-write", ClusterID: "12345", ScheduleTime: &scheduleTime}
	manual := &entity.UserBackup{ID: "manual", ClusterID: "12345"}

	backups := suppressPersistedBackupsWithTombstones(
		[]*entity.UserBackup{deleted, manual},
		map[string]struct{}{backupScheduleKey("12345", scheduleTime): {}},
	)

	require.Equal(t, []*entity.UserBackup{manual}, backups)
}

func TestSuppressPersistedBackupsWithVirtualScheduleOnlyReplacesMatchingAutoBackup(t *testing.T) {
	scheduleTime := time.Date(2026, 7, 8, 10, 0, 0, 0, time.UTC)
	otherScheduleTime := scheduleTime.Add(24 * time.Hour)
	matchingAuto := &entity.UserBackup{
		ID: "matching-auto", Type: constant.BackupTypeAuto, ClusterID: "12345", ScheduleTime: &scheduleTime,
	}
	otherAuto := &entity.UserBackup{
		ID: "other-auto", Type: constant.BackupTypeAuto, ClusterID: "12345", ScheduleTime: &otherScheduleTime,
	}
	manual := &entity.UserBackup{
		ID: "manual", Type: constant.BackupType("manual"), ClusterID: "12345", ScheduleTime: &scheduleTime,
	}
	legacyWithoutSchedule := &entity.UserBackup{
		ID: "legacy", Type: constant.BackupTypeAuto, ClusterID: "12345",
	}
	virtual := &VirtualBackup{
		ClusterID: "12345", ScheduleTime: scheduleTime, Physical: &entity.PhysicalBackupSlot{},
	}

	backups := suppressPersistedBackupsWithVirtualSchedule(
		[]*entity.UserBackup{matchingAuto, otherAuto, manual, legacyWithoutSchedule},
		[]*VirtualBackup{virtual},
	)

	require.Equal(t, []*entity.UserBackup{otherAuto, manual, legacyWithoutSchedule}, backups)
}

func TestSuppressPersistedBackupsWithVirtualScheduleRequiresReadyVirtualBackup(t *testing.T) {
	scheduleTime := time.Date(2026, 7, 8, 10, 0, 0, 0, time.UTC)
	persisted := &entity.UserBackup{
		ID: "persisted", Type: constant.BackupTypeAuto, ClusterID: "12345", ScheduleTime: &scheduleTime,
	}
	virtual := &VirtualBackup{ClusterID: "12345", ScheduleTime: scheduleTime}

	backups := suppressPersistedBackupsWithVirtualSchedule(
		[]*entity.UserBackup{persisted},
		[]*VirtualBackup{virtual},
	)

	require.Equal(t, []*entity.UserBackup{persisted}, backups)
}

func TestSuppressUnreadyVirtualBackupsWithPersistedSchedule(t *testing.T) {
	scheduleTime := time.Date(2026, 7, 8, 10, 0, 0, 0, time.UTC)
	persisted := &entity.UserBackup{
		ID: "persisted", Type: constant.BackupTypeAuto, ClusterID: "12345", ScheduleTime: &scheduleTime,
	}
	unreadyVirtual := &VirtualBackup{ClusterID: "12345", ScheduleTime: scheduleTime}
	readyVirtual := &VirtualBackup{
		ClusterID: "12345", ScheduleTime: scheduleTime, Physical: &entity.PhysicalBackupSlot{},
	}

	backups := suppressUnreadyVirtualBackupsWithPersistedSchedule(
		[]*entity.UserBackup{persisted},
		[]*VirtualBackup{unreadyVirtual, readyVirtual},
	)

	require.Equal(t, []*VirtualBackup{readyVirtual}, backups)
}

func TestHistoryCoversScheduleTime(t *testing.T) {
	validFrom := time.Date(2026, 7, 8, 9, 0, 0, 0, time.UTC)
	validTo := time.Date(2026, 7, 10, 9, 0, 0, 0, time.UTC)
	history := &entity.BackupScheduleHistory{
		ScheduleCron: "0 0 10 * * *",
		ValidFrom:    &validFrom,
		ValidTo:      &validTo,
	}

	require.True(t, historyCoversScheduleTime(history, time.Date(2026, 7, 8, 10, 0, 0, 0, time.UTC)))
	require.True(t, historyCoversScheduleTime(history, time.Date(2026, 7, 9, 10, 0, 0, 0, time.UTC)))
	// A virtual ID is user-controlled; a timestamp inside the validity window
	// must not be accepted unless it is an actual cron occurrence.
	require.False(t, historyCoversScheduleTime(history, time.Date(2026, 7, 9, 10, 0, 1, 0, time.UTC)))
	require.False(t, historyCoversScheduleTime(history, time.Date(2026, 7, 9, 11, 0, 0, 0, time.UTC)))
	require.False(t, historyCoversScheduleTime(history, time.Date(2026, 7, 10, 10, 0, 0, 0, time.UTC)))
	require.False(t, historyCoversScheduleTime(history, time.Date(2026, 7, 8, 8, 0, 0, 0, time.UTC)))
}

func TestSortBackupViewsDefaultsToAscending(t *testing.T) {
	earlier := time.Date(2026, 7, 8, 10, 0, 0, 0, time.UTC)
	later := earlier.Add(time.Hour)
	items := []*vo.Backup{{ID: "later", BackupTime: &later}, {ID: "earlier", BackupTime: &earlier}}

	sortBackupViews(items, "")

	require.Equal(t, []string{"earlier", "later"}, []string{items[0].ID, items[1].ID})
}

func TestSortRecycledInstancesUsesClusterIDDescendingAsTieBreaker(t *testing.T) {
	deletedAt := time.Date(2026, 7, 8, 10, 0, 0, 0, time.UTC)
	instances := []*entity.RecycledInstance{
		{ClusterID: "100", DeletedAt: &deletedAt},
		{ClusterID: "300", DeletedAt: &deletedAt},
		{ClusterID: "200", DeletedAt: &deletedAt},
	}

	sortRecycledInstancesByDeleteTime(instances, "delete_time desc")

	require.Equal(t, []string{"300", "200", "100"}, []string{
		instances[0].ClusterID,
		instances[1].ClusterID,
		instances[2].ClusterID,
	})
}
