package backup

import (
	"context"
	"encoding/json"
	"fmt"
	"sync"
	"testing"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/robfig/cron/v3"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/observability/vmbackupmanager/pkg/config"
)

func createTestConfig() *config.Config {
	return &config.Config{
		StorageDataPath:           "/test/data",
		SnapshotCreateURL:         "http://localhost:8428/snapshot/create",
		Destination:               "fs:///test-destination",
		Concurrency:               2,
		FullBackupConcurrency:     1,
		MaxBytesPerSecond:         "1MB",
		HttpListenAddr:            ":8080",
		IncrSchedule:              "*/5 * * * *",
		FullSchedule:              "0 2 * * *",
		BackupRetention:           7 * 24 * time.Hour,
		VMRetentionPeriod:         24 * time.Hour,
		VMRetentionTimeZoneOffset: 0,
		DisableFullBackup:         false,
		DisableBackupRetention:    false,
	}
}

// newTestManager creates a manager for testing with mock storage
func newTestManager(cfg *config.Config) *Manager {
	ctx, cancel := context.WithCancel(context.Background())

	m := &Manager{
		ctx:       ctx,
		cancel:    cancel,
		config:    cfg,
		scheduler: cron.New(),
		backups:   sync.Map{},
		taskQueue: make(chan *BackupMetadata, 1),
		store:     NewMockStorage(),
	}

	return m
}

func TestNewManager(t *testing.T) {
	cfg := createTestConfig()

	// Test successful creation
	manager, err := NewManager(cfg)
	require.NoError(t, err)
	assert.NotNil(t, manager)
	assert.Equal(t, cfg, manager.config)
	assert.NotNil(t, manager.scheduler)
	assert.NotNil(t, manager.taskQueue)
	assert.Equal(t, 1, cap(manager.taskQueue))
}

func TestManager_SaveMetadata(t *testing.T) {
	cfg := createTestConfig()
	manager := newTestManager(cfg)

	// Get mock storage
	mockStorage := manager.store.(*MockStorage)

	// Create test metadata
	metadata := &BackupMetadata{
		Id:            "test-backup",
		Type:          FullBackup,
		DestPath:      "full/test-backup",
		CreatedAt:     time.Now().Format(time.RFC3339),
		Status:        StatusCompleted,
		DataRetention: "24h",
	}

	// Setup mock expectations for saving
	mockStorage.On("CreateFile", mock.Anything, "backupmgr_metadata/test-backup", mock.AnythingOfType("[]uint8")).Return(nil)

	// Test saving metadata
	err := manager.saveMetadata(metadata)
	require.NoError(t, err)

	// Verify metadata was stored in backups map
	stored, exists := manager.GetBackup("test-backup")
	assert.True(t, exists)
	assert.Equal(t, metadata.Id, stored.Id)
	assert.Equal(t, metadata.Type, stored.Type)

	mockStorage.AssertExpectations(t)
}

func TestManager_LoadMetadata(t *testing.T) {
	cfg := createTestConfig()
	manager := newTestManager(cfg)

	// Get mock storage
	mockStorage := manager.store.(*MockStorage)

	// Create test metadata
	metadata := &BackupMetadata{
		Id:            "test-backup",
		Type:          FullBackup,
		DestPath:      "full/test-backup",
		CreatedAt:     time.Now().Format(time.RFC3339),
		Status:        StatusCompleted,
		DataRetention: "24h",
	}

	// Setup mock for loading metadata
	metadataJSON, _ := json.Marshal(metadata)
	mockStorage.On("ListFiles", mock.Anything, BackupMgrMetadataPath).Return([]string{"backupmgr_metadata/test-backup"}, nil)
	mockStorage.On("ReadFile", mock.Anything, "backupmgr_metadata/test-backup").Return(metadataJSON, nil)

	// Test loading metadata
	err := manager.loadMetadata()
	require.NoError(t, err)

	// Verify metadata was loaded
	loaded, exists := manager.GetBackup("test-backup")
	assert.True(t, exists)
	if exists {
		assert.Equal(t, metadata.Id, loaded.Id)
		assert.Equal(t, metadata.Type, loaded.Type)
		assert.Equal(t, metadata.Status, loaded.Status)
	}

	mockStorage.AssertExpectations(t)
}

func TestManager_SaveAndLoadMetadata(t *testing.T) {
	cfg := createTestConfig()
	manager := newTestManager(cfg)

	// Get mock storage
	mockStorage := manager.store.(*MockStorage)

	// Create test metadata with completed status to avoid ensureEnqueueBackup call
	metadata := &BackupMetadata{
		Id:            "test-backup",
		Type:          FullBackup,
		DestPath:      "full/test-backup",
		CreatedAt:     time.Now().Format(time.RFC3339),
		Status:        StatusCompleted, // Important: use completed status
		DataRetention: "24h",
	}

	// Setup mock expectations for saving
	mockStorage.On("CreateFile", mock.Anything, "backupmgr_metadata/test-backup", mock.AnythingOfType("[]uint8")).Return(nil)

	// Test saving metadata
	err := manager.saveMetadata(metadata)
	require.NoError(t, err)

	// Verify metadata was stored in backups map
	stored, exists := manager.GetBackup("test-backup")
	assert.True(t, exists)
	assert.Equal(t, metadata.Id, stored.Id)
	assert.Equal(t, metadata.Type, stored.Type)

	// Setup mock for loading metadata
	metadataJSON, _ := json.Marshal(metadata)
	mockStorage.On("ListFiles", mock.Anything, BackupMgrMetadataPath).Return([]string{"backupmgr_metadata/test-backup"}, nil)
	mockStorage.On("ReadFile", mock.Anything, "backupmgr_metadata/test-backup").Return(metadataJSON, nil)

	// Clear the backups map to test loading
	manager.backups = sync.Map{}

	// Test loading metadata
	err = manager.loadMetadata()
	require.NoError(t, err)

	// Verify metadata was loaded
	loaded, exists := manager.GetBackup("test-backup")
	assert.True(t, exists)
	if exists {
		assert.Equal(t, metadata.Id, loaded.Id)
		assert.Equal(t, metadata.Type, loaded.Type)
		assert.Equal(t, metadata.Status, loaded.Status)
	}

	mockStorage.AssertExpectations(t)
}

func TestManager_EnqueueBackup(t *testing.T) {
	cfg := createTestConfig()
	manager := newTestManager(cfg)

	// Get mock storage
	mockStorage := manager.store.(*MockStorage)

	// Setup mock expectations
	mockStorage.On("CreateFile", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("[]uint8")).Return(nil)

	tests := []struct {
		name         string
		backupType   BackupType
		backupName   string
		expectedPath string
	}{
		{
			name:         "enqueue full backup",
			backupType:   FullBackup,
			backupName:   "full-2024-01-01",
			expectedPath: "full/full-2024-01-01",
		},
		{
			name:         "enqueue incremental backup",
			backupType:   IncrementalBackup,
			backupName:   "latest",
			expectedPath: "latest",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			metadata, err := manager.EnqueueBackup(tt.backupType, tt.backupName)
			require.NoError(t, err)
			assert.NotNil(t, metadata)
			assert.Equal(t, tt.backupName, metadata.Id)
			assert.Equal(t, tt.backupType, metadata.Type)
			assert.Equal(t, tt.expectedPath, metadata.DestPath)
			assert.Equal(t, StatusRunning, metadata.Status)

			// Verify backup was added to queue
			assert.Equal(t, 1, len(manager.taskQueue))

			// Drain the queue for next test
			<-manager.taskQueue
		})
	}

	mockStorage.AssertExpectations(t)
}

func TestManager_EnqueueBackup_QueueFull(t *testing.T) {
	cfg := createTestConfig()
	manager := newTestManager(cfg)

	// Get mock storage
	mockStorage := manager.store.(*MockStorage)

	// Setup mock expectations
	mockStorage.On("CreateFile", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("[]uint8")).Return(nil)

	// Fill the queue
	_, err := manager.EnqueueBackup(FullBackup, "test1")
	require.NoError(t, err)

	// Try to enqueue another backup when queue is full
	_, err = manager.EnqueueBackup(FullBackup, "test2")
	assert.Equal(t, ErrQueueFull, err)

	mockStorage.AssertExpectations(t)
}

func TestManager_DeleteBackup(t *testing.T) {
	cfg := createTestConfig()
	manager := newTestManager(cfg)

	// Get mock storage
	mockStorage := manager.store.(*MockStorage)

	// Setup mock expectations
	mockStorage.On("DeleteDir", mock.Anything, "full/test-backup").Return(nil)
	mockStorage.On("DeleteFile", mock.Anything, "backupmgr_metadata/test-backup").Return(nil)

	// Create test metadata
	metadata := &BackupMetadata{
		Id:       "test-backup",
		Type:     FullBackup,
		DestPath: "full/test-backup",
		Status:   StatusCompleted,
	}

	// Add to backups map
	manager.backups.Store(metadata.Id, metadata)

	// Test deletion
	err := manager.DeleteBackup(metadata)
	require.NoError(t, err)

	// Verify backup was removed from map
	_, exists := manager.GetBackup("test-backup")
	assert.False(t, exists)

	mockStorage.AssertExpectations(t)
}

func TestManager_ListBackups(t *testing.T) {
	cfg := createTestConfig()
	manager := newTestManager(cfg)

	// Add test backups
	backup1 := &BackupMetadata{Id: "backup1", Type: FullBackup}
	backup2 := &BackupMetadata{Id: "backup2", Type: IncrementalBackup}

	manager.backups.Store("backup1", backup1)
	manager.backups.Store("backup2", backup2)

	// Test listing
	backups := manager.ListBackups()
	assert.Len(t, backups, 2)

	// Verify both backups are present
	ids := make(map[string]bool)
	for _, backup := range backups {
		ids[backup.Id] = true
	}
	assert.True(t, ids["backup1"])
	assert.True(t, ids["backup2"])
}

func TestManager_IsBackupRunning(t *testing.T) {
	cfg := createTestConfig()
	manager := newTestManager(cfg)

	// Initially no backup should be running
	assert.False(t, manager.IsBackupRunning())
	backup, exists := manager.GetRunningBackupIfExists()
	assert.False(t, exists)
	assert.Nil(t, backup)

	// Set a running backup
	runningBackup := &BackupMetadata{Id: "running", Type: FullBackup}
	manager.runningBackup.Store(runningBackup)

	assert.True(t, manager.IsBackupRunning())
	backup, exists = manager.GetRunningBackupIfExists()
	assert.True(t, exists)
	assert.Equal(t, runningBackup, backup)
}

func TestManager_BackupGC(t *testing.T) {
	cfg := createTestConfig()
	cfg.BackupRetention = 24 * time.Hour // 1 day retention
	manager := newTestManager(cfg)

	// Get mock storage
	mockStorage := manager.store.(*MockStorage)

	// Create test backups - one old, one recent
	oldTime := time.Now().Add(-48 * time.Hour)    // 2 days ago
	recentTime := time.Now().Add(-12 * time.Hour) // 12 hours ago

	oldBackup := &BackupMetadata{
		Id:          "old-backup",
		Type:        FullBackup,
		DestPath:    "full/old-backup",
		Status:      StatusCompleted,
		CompletedAt: oldTime.Format(time.RFC3339),
	}

	recentBackup := &BackupMetadata{
		Id:          "recent-backup",
		Type:        FullBackup,
		DestPath:    "full/recent-backup",
		Status:      StatusCompleted,
		CompletedAt: recentTime.Format(time.RFC3339),
	}

	incrBackup := &BackupMetadata{
		Id:          "incr-backup",
		Type:        IncrementalBackup,
		DestPath:    "latest",
		Status:      StatusCompleted,
		CompletedAt: oldTime.Format(time.RFC3339),
	}

	// Add backups to manager
	manager.backups.Store(oldBackup.Id, oldBackup)
	manager.backups.Store(recentBackup.Id, recentBackup)
	manager.backups.Store(incrBackup.Id, incrBackup)

	// Setup mock expectations
	mockStorage.On("Exists", mock.Anything, "full/old-backup").Return(true, nil)
	mockStorage.On("Exists", mock.Anything, "full/recent-backup").Return(true, nil)
	mockStorage.On("Exists", mock.Anything, "latest").Return(true, nil)
	mockStorage.On("DeleteDir", mock.Anything, "full/old-backup").Return(nil)
	mockStorage.On("DeleteFile", mock.Anything, "backupmgr_metadata/old-backup").Return(nil)

	// Run GC
	manager.backupGC()

	// Verify old full backup was deleted
	_, exists := manager.GetBackup("old-backup")
	assert.False(t, exists)

	// Verify recent full backup still exists
	_, exists = manager.GetBackup("recent-backup")
	assert.True(t, exists)

	// Verify incremental backup still exists (should not be deleted by GC)
	_, exists = manager.GetBackup("incr-backup")
	assert.True(t, exists)

	mockStorage.AssertExpectations(t)
}

func TestManager_BackupGC_DisabledRetention(t *testing.T) {
	cfg := createTestConfig()
	cfg.BackupRetention = 0 // Disabled
	manager := newTestManager(cfg)

	// Add old backup
	oldBackup := &BackupMetadata{
		Id:          "old-backup",
		Type:        FullBackup,
		Status:      StatusCompleted,
		CompletedAt: time.Now().Add(-48 * time.Hour).Format(time.RFC3339),
	}
	manager.backups.Store(oldBackup.Id, oldBackup)

	// Run GC - should not delete anything
	manager.backupGC()

	// Verify backup still exists
	_, exists := manager.GetBackup("old-backup")
	assert.True(t, exists)
}

func TestManager_PrometheusMetrics(t *testing.T) {
	cfg := createTestConfig()
	manager := newTestManager(cfg)

	// Add test backup
	backup := &BackupMetadata{
		Id:            "test-backup",
		Type:          FullBackup,
		DestPath:      "full/test-backup",
		CreatedAt:     "2024-01-01T00:00:00Z",
		CompletedAt:   "2024-01-01T01:00:00Z",
		Status:        StatusCompleted,
		DataRetention: "24h",
	}
	manager.backups.Store(backup.Id, backup)

	// Test Describe
	descCh := make(chan *prometheus.Desc, 1)
	manager.Describe(descCh)
	close(descCh)

	desc := <-descCh
	assert.NotNil(t, desc)

	// Test Collect
	metricCh := make(chan prometheus.Metric, 1)
	manager.Collect(metricCh)
	close(metricCh)

	metric := <-metricCh
	assert.NotNil(t, metric)
}

func TestManager_Shutdown(t *testing.T) {
	cfg := createTestConfig()
	manager := newTestManager(cfg)

	// Test shutdown with timeout
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	manager.Shutdown(ctx)

	// Verify context was cancelled
	select {
	case <-manager.ctx.Done():
		// Expected
	default:
		t.Error("Manager context should be cancelled after shutdown")
	}
}

func TestManager_LoadMetadata_ErrorHandling(t *testing.T) {
	cfg := createTestConfig()
	manager := newTestManager(cfg)

	// Get mock storage
	mockStorage := manager.store.(*MockStorage)

	// Setup mock to return error
	mockStorage.On("ListFiles", mock.Anything, BackupMgrMetadataPath).Return(nil, fmt.Errorf("storage error"))

	// Test error handling
	err := manager.loadMetadata()
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "failed to list metadata directory")

	mockStorage.AssertExpectations(t)
}

func TestManager_SaveMetadata_ErrorHandling(t *testing.T) {
	cfg := createTestConfig()
	manager := newTestManager(cfg)

	// Get mock storage
	mockStorage := manager.store.(*MockStorage)

	// Setup mock to return error
	mockStorage.On("CreateFile", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("[]uint8")).Return(fmt.Errorf("storage error"))

	metadata := &BackupMetadata{
		Id:       "test-backup",
		Type:     FullBackup,
		DestPath: "full/test-backup",
		Status:   StatusCompleted,
	}

	// Test error handling
	err := manager.saveMetadata(metadata)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "failed to save metadata")

	mockStorage.AssertExpectations(t)
}

func TestManager_EnsureEnqueueBackup(t *testing.T) {
	cfg := createTestConfig()
	manager := newTestManager(cfg)

	// Get mock storage
	mockStorage := manager.store.(*MockStorage)

	// Setup mock expectations
	mockStorage.On("CreateFile", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("[]uint8")).Return(nil)

	// Test ensure enqueue backup
	manager.ensureEnqueueBackup(FullBackup, "test-backup")

	// Verify backup was enqueued
	assert.Equal(t, 1, len(manager.taskQueue))

	// Verify backup metadata exists
	_, exists := manager.GetBackup("test-backup")
	assert.True(t, exists)

	mockStorage.AssertExpectations(t)
}

func TestManager_IsQueueFull(t *testing.T) {
	cfg := createTestConfig()
	manager := newTestManager(cfg)

	// Initially queue should not be full
	assert.False(t, manager.isQueueFull())

	// Get mock storage
	mockStorage := manager.store.(*MockStorage)
	mockStorage.On("CreateFile", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("[]uint8")).Return(nil)

	// Fill the queue
	_, err := manager.EnqueueBackup(FullBackup, "test")
	require.NoError(t, err)

	// Now queue should be full
	assert.True(t, manager.isQueueFull())

	mockStorage.AssertExpectations(t)
}

func TestManager_BasicOperations(t *testing.T) {
	cfg := createTestConfig()
	manager := newTestManager(cfg)

	// Test basic manager creation
	assert.NotNil(t, manager)
	assert.NotNil(t, manager.config)
	assert.NotNil(t, manager.scheduler)
	assert.NotNil(t, manager.taskQueue)
	assert.Equal(t, 1, cap(manager.taskQueue))

	// Test backup metadata creation
	metadata := &BackupMetadata{
		Id:            "test-backup",
		Type:          FullBackup,
		DestPath:      "full/test-backup",
		CreatedAt:     time.Now().Format(time.RFC3339),
		Status:        StatusCompleted,
		DataRetention: "24h",
	}

	// Test adding backup to map directly
	manager.backups.Store(metadata.Id, metadata)

	// Test GetBackup
	stored, exists := manager.GetBackup("test-backup")
	assert.True(t, exists)
	assert.Equal(t, metadata.Id, stored.Id)
	assert.Equal(t, metadata.Type, stored.Type)

	// Test ListBackups
	backups := manager.ListBackups()
	assert.Len(t, backups, 1)
	assert.Equal(t, metadata.Id, backups[0].Id)

	// Test IsBackupRunning
	assert.False(t, manager.IsBackupRunning())
	manager.runningBackup.Store(metadata)
	assert.True(t, manager.IsBackupRunning())

	// Test isQueueFull
	assert.False(t, manager.isQueueFull())
}

func TestRetentionScheduleIntegrationWithCronScheduler(t *testing.T) {
	// Create a cron scheduler
	scheduler := cron.New()

	// Create retention schedule
	retentionSchedule := &RetentionSchedule{
		RetentionPeriod:         24 * time.Hour,
		RetentionTimeZoneOffset: 0,
	}

	// Track job executions
	var executionTimes []time.Time
	var mu sync.Mutex

	// Create a job that records execution times
	job := cron.FuncJob(func() {
		mu.Lock()
		executionTimes = append(executionTimes, time.Now())
		mu.Unlock()
	})

	// Schedule the job with retention schedule
	scheduler.Schedule(retentionSchedule, job)

	// Start the scheduler
	scheduler.Start()
	defer scheduler.Stop()

	// Test that the schedule was added successfully
	entries := scheduler.Entries()
	assert.Len(t, entries, 1, "Should have one scheduled entry")

	// Verify the schedule is correctly set
	entry := entries[0]
	assert.NotNil(t, entry.Schedule, "Schedule should not be nil")

	// Test Next method on the actual scheduled entry
	now := time.Now()
	nextRun := entry.Schedule.Next(now)
	assert.True(t, nextRun.After(now), "Next run should be in the future")

	// Verify the next run is within expected range (should be within 24 hours + tolerance)
	maxExpectedNext := now.Add(25 * time.Hour) // 24h + 1h tolerance
	assert.True(t, nextRun.Before(maxExpectedNext),
		"Next run %v should be within 25 hours from now %v", nextRun, now)

	t.Logf("Current time: %v", now)
	t.Logf("Next scheduled run: %v", nextRun)
	t.Logf("Time until next run: %v", nextRun.Sub(now))
}
