package engine import ( "testing" "time" ) func TestFormatRelativeDate(t *testing.T) { // Fix timeNow for deterministic tests origTimeNow := timeNow defer func() { timeNow = origTimeNow }() // Wednesday, Feb 18, 2026 at 14:30 local time now := time.Date(2026, 2, 18, 14, 30, 0, 0, time.Local) timeNow = func() time.Time { return now } tests := []struct { name string input time.Time expected string }{ // Core relative dates {"today", time.Date(2026, 2, 18, 0, 0, 0, 0, time.Local), "today"}, {"today with time", time.Date(2026, 2, 18, 23, 59, 0, 0, time.Local), "today"}, {"tomorrow", time.Date(2026, 2, 19, 0, 0, 0, 0, time.Local), "tomorrow"}, {"yesterday", time.Date(2026, 2, 17, 0, 0, 0, 0, time.Local), "yesterday"}, // Near future {"in 2d", time.Date(2026, 2, 20, 0, 0, 0, 0, time.Local), "in 2d"}, {"in 7d", time.Date(2026, 2, 25, 0, 0, 0, 0, time.Local), "in 7d"}, {"in 14d", time.Date(2026, 3, 4, 0, 0, 0, 0, time.Local), "in 14d"}, // Near past {"2d ago", time.Date(2026, 2, 16, 0, 0, 0, 0, time.Local), "2d ago"}, {"7d ago", time.Date(2026, 2, 11, 0, 0, 0, 0, time.Local), "7d ago"}, {"14d ago", time.Date(2026, 2, 4, 0, 0, 0, 0, time.Local), "14d ago"}, // Beyond 14 days - same year {"15d future", time.Date(2026, 3, 5, 0, 0, 0, 0, time.Local), "Mar 5"}, {"15d past", time.Date(2026, 2, 3, 0, 0, 0, 0, time.Local), "Feb 3"}, // Cross-year {"next year", time.Date(2027, 6, 15, 0, 0, 0, 0, time.Local), "Jun 15 2027"}, {"last year", time.Date(2025, 12, 1, 0, 0, 0, 0, time.Local), "Dec 1 2025"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := FormatRelativeDate(tt.input) if result != tt.expected { t.Errorf("FormatRelativeDate(%v) = %q, want %q", tt.input, result, tt.expected) } }) } } func TestFormatRelativeDate_WeekdayPipeline(t *testing.T) { // This test reproduces the reported bug: // On Wednesday, "due:friday" should show "in 2d", not "tomorrow" origTimeNow := timeNow defer func() { timeNow = origTimeNow }() // Wednesday, Feb 18, 2026 wednesday := time.Date(2026, 2, 18, 10, 0, 0, 0, time.Local) timeNow = func() time.Time { return wednesday } parser := NewDateParser(wednesday, time.Monday) tests := []struct { name string weekday string expectedRel string expectedDay time.Weekday }{ {"friday from wednesday", "friday", "in 2d", time.Friday}, {"fri from wednesday", "fri", "in 2d", time.Friday}, {"thursday from wednesday", "thu", "tomorrow", time.Thursday}, {"saturday from wednesday", "sat", "in 3d", time.Saturday}, {"sunday from wednesday", "sun", "in 4d", time.Sunday}, {"monday from wednesday", "mon", "in 5d", time.Monday}, {"tuesday from wednesday", "tue", "in 6d", time.Tuesday}, {"wednesday from wednesday", "wed", "in 7d", time.Wednesday}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { parsed, err := parser.ParseDate(tt.weekday) if err != nil { t.Fatalf("ParseDate(%q) error: %v", tt.weekday, err) } // Verify correct weekday if parsed.Weekday() != tt.expectedDay { t.Errorf("ParseDate(%q) weekday = %v, want %v", tt.weekday, parsed.Weekday(), tt.expectedDay) } // Verify relative display rel := FormatRelativeDate(parsed) if rel != tt.expectedRel { t.Errorf("FormatRelativeDate(ParseDate(%q)) = %q, want %q (parsed date: %v)", tt.weekday, rel, tt.expectedRel, parsed) } }) } } func TestFormatRelativeDate_AllWeekdaysFromAllDays(t *testing.T) { // Exhaustive: parse every weekday name from every starting day of the week origTimeNow := timeNow defer func() { timeNow = origTimeNow }() // Week starting Monday Feb 16 2026 weekStart := time.Date(2026, 2, 16, 12, 0, 0, 0, time.Local) // Monday weekdays := []string{"mon", "tue", "wed", "thu", "fri", "sat", "sun"} targetWeekdays := []time.Weekday{ time.Monday, time.Tuesday, time.Wednesday, time.Thursday, time.Friday, time.Saturday, time.Sunday, } for fromOffset := 0; fromOffset < 7; fromOffset++ { fromDate := weekStart.AddDate(0, 0, fromOffset) fromName := fromDate.Weekday().String() timeNow = func() time.Time { return fromDate } parser := NewDateParser(fromDate, time.Monday) for i, dayName := range weekdays { t.Run(fromName+"_to_"+dayName, func(t *testing.T) { parsed, err := parser.ParseDate(dayName) if err != nil { t.Fatalf("ParseDate(%q) from %s: %v", dayName, fromName, err) } // Must be the correct weekday if parsed.Weekday() != targetWeekdays[i] { t.Errorf("wrong weekday: got %v, want %v", parsed.Weekday(), targetWeekdays[i]) } // Must be in the future (1-7 days from now) diff := parsed.Sub(time.Date(fromDate.Year(), fromDate.Month(), fromDate.Day(), 0, 0, 0, 0, time.Local)) days := int(diff.Hours() / 24) if days < 1 || days > 7 { t.Errorf("ParseDate(%q) from %s: expected 1-7 days ahead, got %d (parsed: %v)", dayName, fromName, days, parsed) } // FormatRelativeDate must match the days offset rel := FormatRelativeDate(parsed) if days == 1 && rel != "tomorrow" { t.Errorf("1 day ahead should be 'tomorrow', got %q", rel) } if days > 1 && days <= 7 { expected := "in " + string(rune('0'+days)) + "d" if days >= 10 { // won't happen for weekdays (max 7) } if rel != expected { t.Errorf("from %s, %q: %d days ahead, got rel=%q, want %q", fromName, dayName, days, rel, expected) } } }) } } } func TestFormatRelativeDate_TimezoneConsistency(t *testing.T) { // Verify that dates in UTC vs Local don't produce wrong relative strings origTimeNow := timeNow defer func() { timeNow = origTimeNow }() now := time.Date(2026, 2, 18, 14, 0, 0, 0, time.Local) timeNow = func() time.Time { return now } // A date 2 days from now, but in UTC targetUTC := time.Date(2026, 2, 20, 0, 0, 0, 0, time.UTC) // Same date in Local targetLocal := time.Date(2026, 2, 20, 0, 0, 0, 0, time.Local) relUTC := FormatRelativeDate(targetUTC) relLocal := FormatRelativeDate(targetLocal) // Both should show "in 2d" - if UTC shows something different, that's a bug if relLocal != "in 2d" { t.Errorf("Local target: expected 'in 2d', got %q", relLocal) } // Note: UTC target may differ depending on system timezone. // This test documents the behavior. t.Logf("Local timezone: now=%v", now) t.Logf("UTC target relative: %q, Local target relative: %q", relUTC, relLocal) if relUTC != relLocal { t.Logf("WARNING: timezone mismatch detected — UTC shows %q vs Local shows %q", relUTC, relLocal) t.Logf("This could explain the 'due:friday shows tomorrow' bug if dates are stored/loaded in wrong timezone") } } func TestFormatDateWithRelative(t *testing.T) { origTimeNow := timeNow defer func() { timeNow = origTimeNow }() now := time.Date(2026, 2, 18, 14, 0, 0, 0, time.Local) timeNow = func() time.Time { return now } input := time.Date(2026, 2, 20, 15, 30, 0, 0, time.Local) result := FormatDateWithRelative(input) // Should contain both absolute and relative if result != "2026-02-20 15:30 (in 2d)" { t.Errorf("FormatDateWithRelative = %q, want %q", result, "2026-02-20 15:30 (in 2d)") } }