package notifications

import (
	"fmt"
	"math/rand"
	"slices"
	"testing"
	"time"
)

// TestReminderScheduleWith100RandomTasks creates 100 tasks with random frequencies
// and validates that notifications are sent on exactly the right days according to
// the current FrequencySchedules and OverdueConfig.
//
// This test is dynamic - it reads from the config, so if the config changes,
// the test will still validate the correct behavior.
func TestReminderScheduleWith100RandomTasks(t *testing.T) {
	// Seed random for reproducibility in tests (use current time for variety)
	rng := rand.New(rand.NewSource(time.Now().UnixNano()))

	// Available frequency intervals (from OrderedFrequencies)
	// We'll also add some custom intervals to test the tier mapping
	frequencyIntervals := []int{0, 1, 7, 14, 30, 90, 180, 365}
	customIntervals := []int{5, 10, 20, 45, 120, 200, 400} // Custom intervals to test tier mapping

	allIntervals := append(frequencyIntervals, customIntervals...)

	// Base date for tests - use a fixed date for deterministic behavior
	baseDate := time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC)

	// Track test statistics
	tasksCreated := 0
	daysSimulated := 0
	notificationsSent := 0
	notificationsExpected := 0

	for i := 0; i < 100; i++ {
		// Random frequency
		intervalIdx := rng.Intn(len(allIntervals))
		intervalDays := allIntervals[intervalIdx]

		// Random due date offset (-30 to +30 days from base date)
		dueDateOffset := rng.Intn(61) - 30
		dueDate := baseDate.AddDate(0, 0, dueDateOffset)

		// Create frequency pointer (nil for "Once" which is interval 0)
		var frequencyDays *int
		if intervalDays > 0 {
			frequencyDays = &intervalDays
		}

		tasksCreated++

		// Get the expected schedule for this frequency
		expectedSchedule := GetScheduleForFrequency(frequencyDays)
		expectedOverdueDays := GetOverdueReminderDays()

		// Simulate each day from 45 days before to 15 days after due date
		for dayOffset := -45; dayOffset <= 15; dayOffset++ {
			today := dueDate.AddDate(0, 0, -dayOffset) // dayOffset is days until due, so we subtract
			daysSimulated++

			// Get the reminder stage for today
			stage := GetReminderStageForToday(dueDate, frequencyDays, today)

			// Calculate what we expect
			expectedStage := calculateExpectedStage(dayOffset, expectedSchedule, expectedOverdueDays)

			if stage != expectedStage {
				t.Errorf("Task %d (interval=%d, dueDate=%s): on %s (dayOffset=%d), got stage=%q, expected=%q",
					i+1, intervalDays, dueDate.Format("2006-01-02"),
					today.Format("2006-01-02"), dayOffset, stage, expectedStage)
			}

			if stage != "" {
				notificationsSent++
			}
			if expectedStage != "" {
				notificationsExpected++
			}
		}
	}

	t.Logf("Test Summary:")
	t.Logf("  Tasks created: %d", tasksCreated)
	t.Logf("  Days simulated: %d", daysSimulated)
	t.Logf("  Notifications sent: %d", notificationsSent)
	t.Logf("  Notifications expected: %d", notificationsExpected)

	if notificationsSent != notificationsExpected {
		t.Errorf("Notification count mismatch: sent=%d, expected=%d", notificationsSent, notificationsExpected)
	}
}

// calculateExpectedStage determines what stage should be returned based on the config
func calculateExpectedStage(dayOffset int, preReminderDays []int, overdueDays []int) string {
	if dayOffset > 0 {
		// Task is in the future (dayOffset days until due)
		if slices.Contains(preReminderDays, dayOffset) {
			return formatDaysBeforeStage(dayOffset)
		}
		return ""
	} else if dayOffset == 0 {
		// Task is due today
		return "day_of"
	} else {
		// Task is overdue (-dayOffset days overdue)
		daysOverdue := -dayOffset
		if slices.Contains(overdueDays, daysOverdue) {
			return formatOverdueStage(daysOverdue)
		}
		return ""
	}
}

// TestEachFrequencySchedule validates each frequency's schedule explicitly
func TestEachFrequencySchedule(t *testing.T) {
	dueDate := time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC)

	// Test each configured frequency
	for intervalDays, expectedSchedule := range FrequencySchedules {
		t.Run(fmt.Sprintf("Interval_%d_days", intervalDays), func(t *testing.T) {
			var frequencyDays *int
			if intervalDays > 0 {
				frequencyDays = &intervalDays
			}

			// Verify all expected reminder days trigger notifications
			for _, reminderDay := range expectedSchedule {
				today := dueDate.AddDate(0, 0, -reminderDay)
				stage := GetReminderStageForToday(dueDate, frequencyDays, today)

				expectedStage := formatDaysBeforeStage(reminderDay)
				if stage != expectedStage {
					t.Errorf("Reminder %d days before: got %q, expected %q", reminderDay, stage, expectedStage)
				}
			}

			// Verify days NOT in the schedule don't trigger notifications (before due date)
			for day := 1; day <= 45; day++ {
				if !slices.Contains(expectedSchedule, day) {
					today := dueDate.AddDate(0, 0, -day)
					stage := GetReminderStageForToday(dueDate, frequencyDays, today)

					if stage != "" {
						t.Errorf("Day %d before due should NOT send notification for interval %d, but got %q",
							day, intervalDays, stage)
					}
				}
			}
		})
	}
}

// TestOverdueReminderSchedule validates the overdue tapering logic
func TestOverdueReminderSchedule(t *testing.T) {
	dueDate := time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC)

	// Use nil frequency (Once) - overdue logic is the same for all frequencies
	var frequencyDays *int

	// Get expected overdue days from config
	expectedOverdueDays := GetOverdueReminderDays()

	t.Logf("OverdueConfig: DailyReminderDays=%d, TaperIntervalDays=%d, MaxOverdueDays=%d",
		OverdueConfig.DailyReminderDays, OverdueConfig.TaperIntervalDays, OverdueConfig.MaxOverdueDays)
	t.Logf("Expected overdue reminder days: %v", expectedOverdueDays)

	// Test each day from 1 to MaxOverdueDays + 5
	for daysOverdue := 1; daysOverdue <= OverdueConfig.MaxOverdueDays+5; daysOverdue++ {
		today := dueDate.AddDate(0, 0, daysOverdue)
		stage := GetReminderStageForToday(dueDate, frequencyDays, today)

		shouldSend := slices.Contains(expectedOverdueDays, daysOverdue)
		expectedStage := ""
		if shouldSend {
			expectedStage = formatOverdueStage(daysOverdue)
		}

		if stage != expectedStage {
			t.Errorf("Day %d overdue: got %q, expected %q (shouldSend=%v)",
				daysOverdue, stage, expectedStage, shouldSend)
		}
	}
}

// TestCustomIntervalTierMapping validates that custom intervals map to correct tiers
func TestCustomIntervalTierMapping(t *testing.T) {
	testCases := []struct {
		customInterval  int
		expectedTier    int
		expectedMessage string
	}{
		{2, 7, "2 days should map to Weekly (7) tier"},
		{5, 7, "5 days should map to Weekly (7) tier"},
		{8, 14, "8 days should map to Bi-Weekly (14) tier"},
		{10, 14, "10 days should map to Bi-Weekly (14) tier"},
		{20, 30, "20 days should map to Monthly (30) tier"},
		{45, 90, "45 days should map to Quarterly (90) tier"},
		{100, 180, "100 days should map to Semi-Annually (180) tier"},
		{200, 365, "200 days should map to Annually (365) tier"},
		{500, 365, "500 days should map to Annually (365) tier - fallback"},
	}

	for _, tc := range testCases {
		t.Run(fmt.Sprintf("CustomInterval_%d", tc.customInterval), func(t *testing.T) {
			schedule := GetScheduleForCustomInterval(tc.customInterval)
			expectedSchedule := FrequencySchedules[tc.expectedTier]

			if !slices.Equal(schedule, expectedSchedule) {
				t.Errorf("%s: got schedule %v, expected %v (tier %d)",
					tc.expectedMessage, schedule, expectedSchedule, tc.expectedTier)
			}
		})
	}
}

// TestDayOfNotification ensures day-of notifications always fire
func TestDayOfNotification(t *testing.T) {
	dueDate := time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC)
	today := dueDate // Same day

	// Test all frequency types - day-of should always fire
	for intervalDays := range FrequencySchedules {
		t.Run(fmt.Sprintf("DayOf_Interval_%d", intervalDays), func(t *testing.T) {
			var frequencyDays *int
			if intervalDays > 0 {
				frequencyDays = &intervalDays
			}

			stage := GetReminderStageForToday(dueDate, frequencyDays, today)
			if stage != "day_of" {
				t.Errorf("Day-of notification for interval %d: got %q, expected \"day_of\"",
					intervalDays, stage)
			}
		})
	}
}

// TestNoNotificationAfterMaxOverdue ensures no notifications after MaxOverdueDays
func TestNoNotificationAfterMaxOverdue(t *testing.T) {
	dueDate := time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC)
	var frequencyDays *int // Once frequency

	// Test days beyond MaxOverdueDays
	for daysOverdue := OverdueConfig.MaxOverdueDays + 1; daysOverdue <= OverdueConfig.MaxOverdueDays+30; daysOverdue++ {
		today := dueDate.AddDate(0, 0, daysOverdue)
		stage := GetReminderStageForToday(dueDate, frequencyDays, today)

		if stage != "" {
			t.Errorf("Day %d overdue (beyond MaxOverdueDays=%d): should not send notification, but got %q",
				daysOverdue, OverdueConfig.MaxOverdueDays, stage)
		}
	}
}

// TestNoNotificationTooFarInFuture ensures no notifications for tasks far in the future
func TestNoNotificationTooFarInFuture(t *testing.T) {
	dueDate := time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC)

	// Get the maximum pre-reminder days from all schedules
	maxPreReminderDays := 0
	for _, schedule := range FrequencySchedules {
		for _, days := range schedule {
			if days > maxPreReminderDays {
				maxPreReminderDays = days
			}
		}
	}

	// Test for each frequency type - days beyond their schedule should not trigger
	for intervalDays, schedule := range FrequencySchedules {
		t.Run(fmt.Sprintf("FarFuture_Interval_%d", intervalDays), func(t *testing.T) {
			var frequencyDays *int
			if intervalDays > 0 {
				frequencyDays = &intervalDays
			}

			// Find the max reminder day for this frequency
			maxForFrequency := 0
			for _, d := range schedule {
				if d > maxForFrequency {
					maxForFrequency = d
				}
			}

			// Test days beyond the max reminder day for this frequency
			for daysBefore := maxForFrequency + 1; daysBefore <= maxPreReminderDays+10; daysBefore++ {
				// Skip if this day is actually in the schedule
				if slices.Contains(schedule, daysBefore) {
					continue
				}

				today := dueDate.AddDate(0, 0, -daysBefore)
				stage := GetReminderStageForToday(dueDate, frequencyDays, today)

				if stage != "" {
					t.Errorf("Day %d before due (beyond schedule max=%d for interval %d): should not send, but got %q",
						daysBefore, maxForFrequency, intervalDays, stage)
				}
			}
		})
	}
}

// TestConfigConsistency validates that the configuration is internally consistent
func TestConfigConsistency(t *testing.T) {
	// Verify GetOverdueReminderDays matches ShouldSendOverdueReminder
	overdueDays := GetOverdueReminderDays()

	for day := 1; day <= OverdueConfig.MaxOverdueDays+5; day++ {
		shouldSend := ShouldSendOverdueReminder(day)
		isInList := slices.Contains(overdueDays, day)

		if shouldSend != isInList {
			t.Errorf("Inconsistency at day %d: ShouldSendOverdueReminder=%v, but day in GetOverdueReminderDays=%v",
				day, shouldSend, isInList)
		}
	}

	// Verify all frequencies have 0 (day-of) in their schedule
	for intervalDays, schedule := range FrequencySchedules {
		if !slices.Contains(schedule, 0) {
			t.Errorf("Frequency %d days is missing day-of (0) in schedule: %v", intervalDays, schedule)
		}
	}

	// Verify schedules are sorted in descending order (most days first)
	for intervalDays, schedule := range FrequencySchedules {
		for i := 0; i < len(schedule)-1; i++ {
			if schedule[i] < schedule[i+1] {
				t.Errorf("Frequency %d schedule is not in descending order: %v", intervalDays, schedule)
				break
			}
		}
	}
}

// TestStageFormatting validates the stage string formatting
func TestStageFormatting(t *testing.T) {
	testCases := []struct {
		daysBefore    int
		expectedStage string
	}{
		{0, "day_of"},
		{1, "reminder_1d"},
		{3, "reminder_3d"},
		{7, "reminder_7d"},
		{14, "reminder_14d"},
		{30, "reminder_30d"},
	}

	for _, tc := range testCases {
		t.Run(fmt.Sprintf("Format_%d_days_before", tc.daysBefore), func(t *testing.T) {
			stage := formatDaysBeforeStage(tc.daysBefore)
			if stage != tc.expectedStage {
				t.Errorf("formatDaysBeforeStage(%d) = %q, expected %q", tc.daysBefore, stage, tc.expectedStage)
			}
		})
	}

	// Test overdue formatting
	overdueTestCases := []struct {
		daysOverdue   int
		expectedStage string
	}{
		{1, "overdue_1"},
		{3, "overdue_3"},
		{7, "overdue_7"},
		{14, "overdue_14"},
	}

	for _, tc := range overdueTestCases {
		t.Run(fmt.Sprintf("Format_%d_days_overdue", tc.daysOverdue), func(t *testing.T) {
			stage := formatOverdueStage(tc.daysOverdue)
			if stage != tc.expectedStage {
				t.Errorf("formatOverdueStage(%d) = %q, expected %q", tc.daysOverdue, stage, tc.expectedStage)
			}
		})
	}
}

// BenchmarkGetReminderStageForToday benchmarks the main function
func BenchmarkGetReminderStageForToday(b *testing.B) {
	dueDate := time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC)
	today := time.Date(2025, 6, 12, 0, 0, 0, 0, time.UTC) // 3 days before
	intervalDays := 30
	frequencyDays := &intervalDays

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		GetReminderStageForToday(dueDate, frequencyDays, today)
	}
}

// TestReminderScheduleTableDriven uses the config to generate a comprehensive
// table-driven test that validates all expected notification days
func TestReminderScheduleTableDriven(t *testing.T) {
	dueDate := time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC)

	// Build test cases from config
	for intervalDays, preSchedule := range FrequencySchedules {
		var frequencyDays *int
		if intervalDays > 0 {
			frequencyDays = &intervalDays
		}

		freqName := FrequencyNames[intervalDays]
		if freqName == "" {
			freqName = fmt.Sprintf("Custom_%d", intervalDays)
		}

		// Test pre-due reminders
		for _, daysBefore := range preSchedule {
			t.Run(fmt.Sprintf("%s_PreReminder_%dd", freqName, daysBefore), func(t *testing.T) {
				today := dueDate.AddDate(0, 0, -daysBefore)
				stage := GetReminderStageForToday(dueDate, frequencyDays, today)
				expected := formatDaysBeforeStage(daysBefore)

				if stage != expected {
					t.Errorf("got %q, expected %q", stage, expected)
				}
			})
		}

		// Test overdue reminders (same for all frequencies)
		overdueDays := GetOverdueReminderDays()
		for _, daysOverdue := range overdueDays {
			t.Run(fmt.Sprintf("%s_Overdue_%dd", freqName, daysOverdue), func(t *testing.T) {
				today := dueDate.AddDate(0, 0, daysOverdue)
				stage := GetReminderStageForToday(dueDate, frequencyDays, today)
				expected := formatOverdueStage(daysOverdue)

				if stage != expected {
					t.Errorf("got %q, expected %q", stage, expected)
				}
			})
		}
	}
}

// TestFullSimulation100Tasks is the main comprehensive test that simulates
// 100 random tasks over the full date range
func TestFullSimulation100Tasks(t *testing.T) {
	// Use a fixed seed for reproducible tests
	rng := rand.New(rand.NewSource(42))

	// All available frequencies including custom
	frequencies := []int{0, 1, 7, 14, 30, 90, 180, 365, 5, 10, 20, 45, 100, 200, 500}

	baseDate := time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC)

	// Statistics
	totalDays := 0
	totalNotifications := 0
	notificationsByStage := make(map[string]int)

	for taskIdx := 0; taskIdx < 100; taskIdx++ {
		// Random frequency
		intervalDays := frequencies[rng.Intn(len(frequencies))]

		// Random due date (within 60-day range around base)
		dueDateOffset := rng.Intn(61) - 30
		dueDate := baseDate.AddDate(0, 0, dueDateOffset)

		var frequencyDays *int
		if intervalDays > 0 {
			frequencyDays = &intervalDays
		}

		// Get expected schedules from config
		preSchedule := GetScheduleForFrequency(frequencyDays)
		overdueDays := GetOverdueReminderDays()

		// Simulate 45 days before to 15 days after due date
		for dayOffset := -45; dayOffset <= 15; dayOffset++ {
			// dayOffset: negative = future (before due), positive = past (overdue)
			// So if dayOffset = -10, we're 10 days BEFORE due date
			// If dayOffset = 5, we're 5 days AFTER due date (overdue)
			today := dueDate.AddDate(0, 0, dayOffset)
			totalDays++

			stage := GetReminderStageForToday(dueDate, frequencyDays, today)

			// Calculate expected stage
			var expectedStage string
			if dayOffset < 0 {
				// Before due date: check pre-reminders
				daysBefore := -dayOffset
				if slices.Contains(preSchedule, daysBefore) {
					expectedStage = formatDaysBeforeStage(daysBefore)
				}
			} else if dayOffset == 0 {
				// Day of
				expectedStage = "day_of"
			} else {
				// Overdue
				if slices.Contains(overdueDays, dayOffset) {
					expectedStage = formatOverdueStage(dayOffset)
				}
			}

			if stage != expectedStage {
				t.Errorf("Task %d (freq=%d, due=%s): day %s (offset=%d): got %q, expected %q",
					taskIdx+1, intervalDays, dueDate.Format("2006-01-02"),
					today.Format("2006-01-02"), dayOffset, stage, expectedStage)
			}

			if stage != "" {
				totalNotifications++
				notificationsByStage[stage]++
			}
		}
	}

	t.Logf("Full Simulation Results:")
	t.Logf("  Total tasks: 100")
	t.Logf("  Total days simulated: %d", totalDays)
	t.Logf("  Total notifications: %d", totalNotifications)
	t.Logf("  Notifications by stage:")
	for stage, count := range notificationsByStage {
		t.Logf("    %s: %d", stage, count)
	}
}
