package alerts

import (
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"
)

// newTestHistoryManager creates a HistoryManager using a temp directory
// that is automatically cleaned up after the test.
func newTestHistoryManager(t *testing.T) *HistoryManager {
	t.Helper()

	tempDir := t.TempDir()

	// Create minimal HistoryManager without starting goroutines
	hm := &HistoryManager{
		dataDir:      tempDir,
		historyFile:  filepath.Join(tempDir, HistoryFileName),
		backupFile:   filepath.Join(tempDir, HistoryBackupFileName),
		history:      make([]HistoryEntry, 0),
		saveInterval: 5 * time.Minute,
		stopChan:     make(chan struct{}),
	}

	return hm
}

func TestGetStats_EmptyHistory(t *testing.T) {
	t.Parallel()

	hm := newTestHistoryManager(t)

	stats := hm.GetStats()

	if stats["totalEntries"].(int) != 0 {
		t.Errorf("totalEntries = %v, want 0", stats["totalEntries"])
	}
	if stats["dataDir"].(string) != hm.dataDir {
		t.Errorf("dataDir = %v, want %v", stats["dataDir"], hm.dataDir)
	}
	if stats["fileSize"].(int64) != 0 {
		t.Errorf("fileSize = %v, want 0", stats["fileSize"])
	}
}

func TestGetStats_WithHistory(t *testing.T) {
	t.Parallel()

	hm := newTestHistoryManager(t)

	// Add some history entries
	now := time.Now()
	hm.history = []HistoryEntry{
		{Alert: Alert{ID: "alert1"}, Timestamp: now.Add(-1 * time.Hour)},
		{Alert: Alert{ID: "alert2"}, Timestamp: now.Add(-30 * time.Minute)},
		{Alert: Alert{ID: "alert3"}, Timestamp: now},
	}

	stats := hm.GetStats()

	if stats["totalEntries"].(int) != 3 {
		t.Errorf("totalEntries = %v, want 3", stats["totalEntries"])
	}

	oldest := stats["oldestEntry"].(time.Time)
	if !oldest.Equal(hm.history[0].Timestamp) {
		t.Errorf("oldestEntry = %v, want %v", oldest, hm.history[0].Timestamp)
	}

	newest := stats["newestEntry"].(time.Time)
	if !newest.Equal(hm.history[2].Timestamp) {
		t.Errorf("newestEntry = %v, want %v", newest, hm.history[2].Timestamp)
	}
}

func TestGetFileSize_NonExistent(t *testing.T) {
	t.Parallel()

	hm := newTestHistoryManager(t)

	size := hm.getFileSize()
	if size != 0 {
		t.Errorf("getFileSize for non-existent file = %v, want 0", size)
	}
}

func TestGetFileSize_ExistingFile(t *testing.T) {
	t.Parallel()

	hm := newTestHistoryManager(t)

	// Create a file with known content
	content := []byte(`[{"alert":{"id":"test"},"timestamp":"2024-01-01T00:00:00Z"}]`)
	if err := os.WriteFile(hm.historyFile, content, 0644); err != nil {
		t.Fatalf("Failed to create test file: %v", err)
	}

	size := hm.getFileSize()
	if size != int64(len(content)) {
		t.Errorf("getFileSize = %v, want %v", size, len(content))
	}
}

func TestAddAlert(t *testing.T) {
	t.Parallel()

	hm := newTestHistoryManager(t)

	alert := Alert{
		ID:           "test-alert-1",
		Type:         "cpu",
		Level:        AlertLevelWarning,
		ResourceName: "pve1",
		Message:      "CPU high",
	}

	hm.AddAlert(alert)

	if len(hm.history) != 1 {
		t.Fatalf("history length = %d, want 1", len(hm.history))
	}

	if hm.history[0].Alert.ID != "test-alert-1" {
		t.Errorf("alert ID = %s, want test-alert-1", hm.history[0].Alert.ID)
	}
}

func TestGetHistory_WithLimit(t *testing.T) {
	t.Parallel()

	hm := newTestHistoryManager(t)

	now := time.Now()
	hm.history = []HistoryEntry{
		{Alert: Alert{ID: "alert1"}, Timestamp: now.Add(-2 * time.Hour)},
		{Alert: Alert{ID: "alert2"}, Timestamp: now.Add(-1 * time.Hour)},
		{Alert: Alert{ID: "alert3"}, Timestamp: now.Add(-30 * time.Minute)},
		{Alert: Alert{ID: "alert4"}, Timestamp: now.Add(-10 * time.Minute)},
	}

	// Get last 2 entries
	results := hm.GetHistory(time.Time{}, 2)

	if len(results) != 2 {
		t.Fatalf("len(results) = %d, want 2", len(results))
	}

	// Should be newest first
	if results[0].ID != "alert4" {
		t.Errorf("first result ID = %s, want alert4", results[0].ID)
	}
	if results[1].ID != "alert3" {
		t.Errorf("second result ID = %s, want alert3", results[1].ID)
	}
}

func TestGetHistory_WithSinceFilter(t *testing.T) {
	t.Parallel()

	hm := newTestHistoryManager(t)

	now := time.Now()
	hm.history = []HistoryEntry{
		{Alert: Alert{ID: "alert1"}, Timestamp: now.Add(-2 * time.Hour)},
		{Alert: Alert{ID: "alert2"}, Timestamp: now.Add(-1 * time.Hour)},
		{Alert: Alert{ID: "alert3"}, Timestamp: now.Add(-30 * time.Minute)},
		{Alert: Alert{ID: "alert4"}, Timestamp: now.Add(-10 * time.Minute)},
	}

	// Get entries from last 45 minutes
	since := now.Add(-45 * time.Minute)
	results := hm.GetHistory(since, 0)

	if len(results) != 2 {
		t.Fatalf("len(results) = %d, want 2 (alert3 and alert4)", len(results))
	}
}

func TestGetAllHistory(t *testing.T) {
	t.Parallel()

	hm := newTestHistoryManager(t)

	now := time.Now()
	hm.history = []HistoryEntry{
		{Alert: Alert{ID: "alert1"}, Timestamp: now.Add(-2 * time.Hour)},
		{Alert: Alert{ID: "alert2"}, Timestamp: now.Add(-1 * time.Hour)},
		{Alert: Alert{ID: "alert3"}, Timestamp: now.Add(-30 * time.Minute)},
	}

	results := hm.GetAllHistory(0)

	if len(results) != 3 {
		t.Fatalf("len(results) = %d, want 3", len(results))
	}

	// Should be newest first
	if results[0].ID != "alert3" {
		t.Errorf("first result ID = %s, want alert3", results[0].ID)
	}
}

func TestGetAllHistory_WithLimit(t *testing.T) {
	t.Parallel()

	hm := newTestHistoryManager(t)

	now := time.Now()
	hm.history = []HistoryEntry{
		{Alert: Alert{ID: "alert1"}, Timestamp: now.Add(-2 * time.Hour)},
		{Alert: Alert{ID: "alert2"}, Timestamp: now.Add(-1 * time.Hour)},
		{Alert: Alert{ID: "alert3"}, Timestamp: now.Add(-30 * time.Minute)},
	}

	results := hm.GetAllHistory(2)

	if len(results) != 2 {
		t.Fatalf("len(results) = %d, want 2", len(results))
	}

	// Should be newest 2, with newest first
	if results[0].ID != "alert3" {
		t.Errorf("first result ID = %s, want alert3", results[0].ID)
	}
	if results[1].ID != "alert2" {
		t.Errorf("second result ID = %s, want alert2", results[1].ID)
	}
}

func TestRemoveAlert(t *testing.T) {
	t.Parallel()

	hm := newTestHistoryManager(t)

	now := time.Now()
	hm.history = []HistoryEntry{
		{Alert: Alert{ID: "alert1"}, Timestamp: now.Add(-2 * time.Hour)},
		{Alert: Alert{ID: "alert2"}, Timestamp: now.Add(-1 * time.Hour)},
		{Alert: Alert{ID: "alert3"}, Timestamp: now.Add(-30 * time.Minute)},
	}

	hm.RemoveAlert("alert2")

	if len(hm.history) != 2 {
		t.Fatalf("history length = %d, want 2", len(hm.history))
	}

	for _, entry := range hm.history {
		if entry.Alert.ID == "alert2" {
			t.Error("alert2 should have been removed")
		}
	}
}

func TestRemoveAlert_NotFound(t *testing.T) {
	t.Parallel()

	hm := newTestHistoryManager(t)

	hm.history = []HistoryEntry{
		{Alert: Alert{ID: "alert1"}, Timestamp: time.Now()},
	}

	// Should not panic or error
	hm.RemoveAlert("nonexistent")

	if len(hm.history) != 1 {
		t.Errorf("history length = %d, want 1", len(hm.history))
	}
}

func TestClearAllHistory(t *testing.T) {
	t.Parallel()

	hm := newTestHistoryManager(t)

	// Add some history
	hm.history = []HistoryEntry{
		{Alert: Alert{ID: "alert1"}, Timestamp: time.Now()},
		{Alert: Alert{ID: "alert2"}, Timestamp: time.Now()},
	}

	// Create files
	_ = os.WriteFile(hm.historyFile, []byte("[]"), 0644)
	_ = os.WriteFile(hm.backupFile, []byte("[]"), 0644)

	err := hm.ClearAllHistory()
	if err != nil {
		t.Fatalf("ClearAllHistory error: %v", err)
	}

	if len(hm.history) != 0 {
		t.Errorf("history length = %d, want 0", len(hm.history))
	}

	// Files should be removed
	if _, err := os.Stat(hm.historyFile); !os.IsNotExist(err) {
		t.Error("history file should be removed")
	}
	if _, err := os.Stat(hm.backupFile); !os.IsNotExist(err) {
		t.Error("backup file should be removed")
	}
}

func TestCleanOldEntries(t *testing.T) {
	t.Parallel()

	hm := newTestHistoryManager(t)

	now := time.Now()
	hm.history = []HistoryEntry{
		{Alert: Alert{ID: "old1"}, Timestamp: now.AddDate(0, 0, -40)},    // 40 days old - should be removed
		{Alert: Alert{ID: "old2"}, Timestamp: now.AddDate(0, 0, -35)},    // 35 days old - should be removed
		{Alert: Alert{ID: "recent1"}, Timestamp: now.AddDate(0, 0, -25)}, // 25 days old - should stay
		{Alert: Alert{ID: "recent2"}, Timestamp: now.AddDate(0, 0, -1)},  // 1 day old - should stay
	}

	hm.cleanOldEntries()

	if len(hm.history) != 2 {
		t.Fatalf("history length = %d, want 2", len(hm.history))
	}

	// Check that only recent entries remain
	for _, entry := range hm.history {
		if entry.Alert.ID == "old1" || entry.Alert.ID == "old2" {
			t.Errorf("old entry %s should have been removed", entry.Alert.ID)
		}
	}
}

func TestSaveHistory(t *testing.T) {
	t.Parallel()

	hm := newTestHistoryManager(t)

	now := time.Now()
	hm.history = []HistoryEntry{
		{Alert: Alert{ID: "alert1", Type: "cpu"}, Timestamp: now},
		{Alert: Alert{ID: "alert2", Type: "memory"}, Timestamp: now},
	}

	err := hm.saveHistory()
	if err != nil {
		t.Fatalf("saveHistory error: %v", err)
	}

	// Verify file was created
	if _, err := os.Stat(hm.historyFile); os.IsNotExist(err) {
		t.Error("history file should exist after save")
	}

	// Read file and verify content
	data, err := os.ReadFile(hm.historyFile)
	if err != nil {
		t.Fatalf("Failed to read history file: %v", err)
	}

	if len(data) == 0 {
		t.Error("history file should not be empty")
	}
}

func TestLoadHistory_NonExistent(t *testing.T) {
	t.Parallel()

	hm := newTestHistoryManager(t)

	err := hm.loadHistory()
	if err != nil {
		t.Fatalf("loadHistory error: %v", err)
	}

	if len(hm.history) != 0 {
		t.Errorf("history should be empty for non-existent files, got %d entries", len(hm.history))
	}
}

func TestLoadHistory_FromMainFile(t *testing.T) {
	t.Parallel()

	hm := newTestHistoryManager(t)

	// Create a history file with a recent timestamp (within MaxHistoryDays)
	recentTime := time.Now().Add(-24 * time.Hour).Format(time.RFC3339)
	content := `[{"alert":{"id":"test-1","type":"cpu"},"timestamp":"` + recentTime + `"}]`
	if err := os.WriteFile(hm.historyFile, []byte(content), 0644); err != nil {
		t.Fatalf("Failed to create test file: %v", err)
	}

	err := hm.loadHistory()
	if err != nil {
		t.Fatalf("loadHistory error: %v", err)
	}

	if len(hm.history) != 1 {
		t.Fatalf("history length = %d, want 1", len(hm.history))
	}

	if hm.history[0].Alert.ID != "test-1" {
		t.Errorf("alert ID = %s, want test-1", hm.history[0].Alert.ID)
	}
}

func TestLoadHistory_FromBackupFile(t *testing.T) {
	t.Parallel()

	hm := newTestHistoryManager(t)

	// Only create backup file with a recent timestamp
	recentTime := time.Now().Add(-24 * time.Hour).Format(time.RFC3339)
	content := `[{"alert":{"id":"backup-1","type":"memory"},"timestamp":"` + recentTime + `"}]`
	if err := os.WriteFile(hm.backupFile, []byte(content), 0644); err != nil {
		t.Fatalf("Failed to create backup file: %v", err)
	}

	err := hm.loadHistory()
	if err != nil {
		t.Fatalf("loadHistory error: %v", err)
	}

	if len(hm.history) != 1 {
		t.Fatalf("history length = %d, want 1", len(hm.history))
	}

	if hm.history[0].Alert.ID != "backup-1" {
		t.Errorf("alert ID = %s, want backup-1", hm.history[0].Alert.ID)
	}
}

func TestLoadHistory_InvalidJSON(t *testing.T) {
	t.Parallel()

	hm := newTestHistoryManager(t)

	// Create an invalid JSON file
	if err := os.WriteFile(hm.historyFile, []byte("not valid json{"), 0644); err != nil {
		t.Fatalf("Failed to create test file: %v", err)
	}

	err := hm.loadHistory()
	if err == nil {
		t.Error("loadHistory should return error for invalid JSON")
	}
}

func TestSaveHistoryWithRetry_Success(t *testing.T) {
	t.Parallel()

	hm := newTestHistoryManager(t)

	hm.history = []HistoryEntry{
		{Alert: Alert{ID: "alert1"}, Timestamp: time.Now()},
	}

	err := hm.saveHistoryWithRetry(3)
	if err != nil {
		t.Fatalf("saveHistoryWithRetry error: %v", err)
	}

	// Verify file was created
	if _, err := os.Stat(hm.historyFile); os.IsNotExist(err) {
		t.Error("history file should exist after save")
	}
}

func TestSaveHistory_CreatesBackup(t *testing.T) {
	t.Parallel()

	hm := newTestHistoryManager(t)

	// Create initial history file
	if err := os.WriteFile(hm.historyFile, []byte("[]"), 0644); err != nil {
		t.Fatalf("Failed to create initial file: %v", err)
	}

	hm.history = []HistoryEntry{
		{Alert: Alert{ID: "new-alert"}, Timestamp: time.Now()},
	}

	err := hm.saveHistory()
	if err != nil {
		t.Fatalf("saveHistory error: %v", err)
	}

	// Backup should be removed after successful save (backup is only for recovery)
	if _, err := os.Stat(hm.backupFile); !os.IsNotExist(err) {
		t.Error("backup file should be removed after successful save")
	}
}

func TestSaveHistoryWithRetry_CreatesBackup(t *testing.T) {
	t.Parallel()

	hm := newTestHistoryManager(t)

	// Create existing history file
	existingContent := `[{"alert":{"id":"existing-alert"},"timestamp":"2025-01-01T00:00:00Z"}]`
	if err := os.WriteFile(hm.historyFile, []byte(existingContent), 0644); err != nil {
		t.Fatalf("Failed to create initial file: %v", err)
	}

	hm.history = []HistoryEntry{
		{Alert: Alert{ID: "new-alert"}, Timestamp: time.Now()},
	}

	err := hm.saveHistoryWithRetry(3)
	if err != nil {
		t.Fatalf("saveHistoryWithRetry error: %v", err)
	}

	// Backup should be removed after successful save (backup is only for recovery)
	if _, err := os.Stat(hm.backupFile); !os.IsNotExist(err) {
		t.Error("backup file should be removed after successful save")
	}

	// Verify the main file was written correctly
	mainData, err := os.ReadFile(hm.historyFile)
	if err != nil {
		t.Fatalf("Failed to read main file: %v", err)
	}

	if !strings.Contains(string(mainData), "new-alert") {
		t.Errorf("main file should contain new-alert, got: %s", mainData)
	}
}

func TestSaveHistoryWithRetry_EmptyHistory(t *testing.T) {
	t.Parallel()

	hm := newTestHistoryManager(t)
	hm.history = []HistoryEntry{}

	err := hm.saveHistoryWithRetry(3)
	if err != nil {
		t.Fatalf("saveHistoryWithRetry error: %v", err)
	}

	data, err := os.ReadFile(hm.historyFile)
	if err != nil {
		t.Fatalf("Failed to read history file: %v", err)
	}

	if string(data) != "[]" {
		t.Errorf("empty history should write [], got %s", data)
	}
}

func TestSaveHistoryWithRetry_SingleRetry(t *testing.T) {
	t.Parallel()

	hm := newTestHistoryManager(t)
	hm.history = []HistoryEntry{
		{Alert: Alert{ID: "test-alert"}, Timestamp: time.Now()},
	}

	// With maxRetries=1, should still succeed on first attempt
	err := hm.saveHistoryWithRetry(1)
	if err != nil {
		t.Fatalf("saveHistoryWithRetry with 1 retry should succeed: %v", err)
	}

	if _, err := os.Stat(hm.historyFile); os.IsNotExist(err) {
		t.Error("history file should exist")
	}
}

func TestSaveHistoryWithRetry_ReadOnlyDirectory(t *testing.T) {
	t.Parallel()

	tempDir := t.TempDir()

	// Create subdirectory and make it read-only
	readOnlyDir := filepath.Join(tempDir, "readonly")
	if err := os.MkdirAll(readOnlyDir, 0755); err != nil {
		t.Fatalf("Failed to create readonly dir: %v", err)
	}

	hm := &HistoryManager{
		dataDir:     readOnlyDir,
		historyFile: filepath.Join(readOnlyDir, HistoryFileName),
		backupFile:  filepath.Join(readOnlyDir, HistoryBackupFileName),
		history:     []HistoryEntry{{Alert: Alert{ID: "test"}, Timestamp: time.Now()}},
		stopChan:    make(chan struct{}),
	}

	// Make directory read-only
	if err := os.Chmod(readOnlyDir, 0444); err != nil {
		t.Fatalf("Failed to make dir readonly: %v", err)
	}
	t.Cleanup(func() {
		os.Chmod(readOnlyDir, 0755) // Restore for cleanup
	})

	// Should fail after retries
	err := hm.saveHistoryWithRetry(2)
	if err == nil {
		t.Error("saveHistoryWithRetry should fail on read-only directory")
	}
}

func TestSaveHistoryWithRetry_ConcurrentSaves(t *testing.T) {
	t.Parallel()

	hm := newTestHistoryManager(t)

	// Populate some history
	for i := 0; i < 10; i++ {
		hm.history = append(hm.history, HistoryEntry{
			Alert:     Alert{ID: "alert-" + string(rune('0'+i))},
			Timestamp: time.Now(),
		})
	}

	// Run multiple concurrent saves - the saveMu lock should serialize them
	errChan := make(chan error, 5)
	for i := 0; i < 5; i++ {
		go func() {
			errChan <- hm.saveHistoryWithRetry(3)
		}()
	}

	// Collect errors
	for i := 0; i < 5; i++ {
		if err := <-errChan; err != nil {
			t.Errorf("concurrent save %d failed: %v", i, err)
		}
	}

	// Verify file exists and is valid
	if _, err := os.Stat(hm.historyFile); os.IsNotExist(err) {
		t.Error("history file should exist after concurrent saves")
	}
}

func TestSaveHistoryWithRetry_SnapshotIsolation(t *testing.T) {
	t.Parallel()

	hm := newTestHistoryManager(t)
	hm.history = []HistoryEntry{
		{Alert: Alert{ID: "original-alert"}, Timestamp: time.Now()},
	}

	// Start save in goroutine
	done := make(chan error)
	go func() {
		done <- hm.saveHistoryWithRetry(3)
	}()

	// Modify history while save might be in progress
	// This shouldn't affect the saved content because saveHistoryWithRetry
	// takes a snapshot under lock
	time.Sleep(1 * time.Millisecond)
	hm.mu.Lock()
	hm.history = append(hm.history, HistoryEntry{
		Alert:     Alert{ID: "added-during-save"},
		Timestamp: time.Now(),
	})
	hm.mu.Unlock()

	if err := <-done; err != nil {
		t.Fatalf("saveHistoryWithRetry error: %v", err)
	}

	// The file should have been written successfully
	if _, err := os.Stat(hm.historyFile); os.IsNotExist(err) {
		t.Error("history file should exist")
	}
}
