package engine import ( "testing" "time" ) func TestParseMonthName(t *testing.T) { // Base: Jan 5, 2026 base := time.Date(2026, 1, 5, 12, 0, 0, 0, time.UTC) parser := NewDateParser(base, time.Monday) tests := []struct { name string input string expected time.Time }{ {"jan (passed)", "jan", time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC)}, {"january (passed)", "january", time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC)}, {"feb (future)", "feb", time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC)}, {"february (future)", "february", time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC)}, {"mar", "mar", time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC)}, {"march", "march", time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC)}, {"dec", "dec", time.Date(2026, 12, 1, 0, 0, 0, 0, time.UTC)}, {"december", "december", time.Date(2026, 12, 1, 0, 0, 0, 0, time.UTC)}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, ok := parser.parseMonthName(tt.input) if !ok { t.Fatalf("Failed to parse month name: %s", tt.input) } if !result.Equal(tt.expected) { t.Errorf("Expected %v, got %v", tt.expected, result) } }) } } func TestParseDayMonth(t *testing.T) { // Base: Jan 5, 2026 base := time.Date(2026, 1, 5, 12, 0, 0, 0, time.UTC) parser := NewDateParser(base, time.Monday) tests := []struct { name string input string expected time.Time }{ {"21jan (future this year)", "21jan", time.Date(2026, 1, 21, 0, 0, 0, 0, time.UTC)}, {"21Jan", "21Jan", time.Date(2026, 1, 21, 0, 0, 0, 0, time.UTC)}, {"Jan21", "Jan21", time.Date(2026, 1, 21, 0, 0, 0, 0, time.UTC)}, {"jan21", "jan21", time.Date(2026, 1, 21, 0, 0, 0, 0, time.UTC)}, {"1jan (passed)", "1jan", time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC)}, {"30dec", "30dec", time.Date(2026, 12, 30, 0, 0, 0, 0, time.UTC)}, {"15feb", "15feb", time.Date(2026, 2, 15, 0, 0, 0, 0, time.UTC)}, {"15February", "15February", time.Date(2026, 2, 15, 0, 0, 0, 0, time.UTC)}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, ok := parser.parseDayMonth(tt.input) if !ok { t.Fatalf("Failed to parse day+month: %s", tt.input) } if !result.Equal(tt.expected) { t.Errorf("Expected %v, got %v", tt.expected, result) } }) } } func TestParsePeriodBoundary(t *testing.T) { // Base: Monday, Jan 5, 2026, 14:30:00 base := time.Date(2026, 1, 5, 14, 30, 0, 0, time.UTC) parser := NewDateParser(base, time.Monday) tests := []struct { name string input string expected time.Time }{ {"sod", "sod", time.Date(2026, 1, 5, 0, 0, 0, 0, time.UTC)}, {"eod", "eod", time.Date(2026, 1, 5, 23, 59, 59, 0, time.UTC)}, {"sow", "sow", time.Date(2026, 1, 12, 0, 0, 0, 0, time.UTC)}, // Next Monday {"eow", "eow", time.Date(2026, 1, 11, 23, 59, 59, 0, time.UTC)}, // Next Sunday {"som", "som", time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC)}, // Next month (Jan 1 passed) {"eom", "eom", time.Date(2026, 1, 31, 23, 59, 59, 0, time.UTC)}, // End of Jan {"soy", "soy", time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC)}, // Next year (Jan 1 passed) {"eoy", "eoy", time.Date(2026, 12, 31, 23, 59, 59, 0, time.UTC)}, // End of 2026 } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, ok := parser.parsePeriodBoundary(tt.input) if !ok { t.Fatalf("Failed to parse period boundary: %s", tt.input) } if !result.Equal(tt.expected) { t.Errorf("Expected %v, got %v", tt.expected, result) } }) } } func TestParseSpecialKeyword(t *testing.T) { base := time.Date(2026, 1, 5, 12, 0, 0, 0, time.UTC) parser := NewDateParser(base, time.Monday) tests := []struct { name string input string expected time.Time }{ {"later", "later", time.Date(2150, 1, 1, 0, 0, 0, 0, time.UTC)}, {"someday", "someday", time.Date(2150, 1, 1, 0, 0, 0, 0, time.UTC)}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, ok := parser.parseSpecialKeyword(tt.input) if !ok { t.Fatalf("Failed to parse special keyword: %s", tt.input) } if !result.Equal(tt.expected) { t.Errorf("Expected %v, got %v", tt.expected, result) } }) } } func TestParseDurationAsDate(t *testing.T) { base := time.Date(2026, 1, 5, 12, 0, 0, 0, time.UTC) parser := NewDateParser(base, time.Monday) tests := []struct { name string input string expected time.Time }{ {"2d", "2d", base.AddDate(0, 0, 2)}, {"3w", "3w", base.AddDate(0, 0, 21)}, {"1m", "1m", base.AddDate(0, 0, 30)}, {"1y", "1y", base.AddDate(0, 0, 365)}, {"daily", "daily", base.AddDate(0, 0, 1)}, {"weekly", "weekly", base.AddDate(0, 0, 7)}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, ok := parser.parseDurationAsDate(tt.input) if !ok { t.Fatalf("Failed to parse duration as date: %s", tt.input) } if !result.Equal(tt.expected) { t.Errorf("Expected %v, got %v", tt.expected, result) } }) } } func TestParseTimeOfDay(t *testing.T) { base := time.Date(2026, 1, 5, 12, 0, 0, 0, time.UTC) parser := NewDateParser(base, time.Monday) tests := []struct { name string date time.Time timeStr string expected time.Time wantErr bool }{ {"HH:MM format", base, "15:35", time.Date(2026, 1, 5, 15, 35, 0, 0, time.UTC), false}, {"HHMM format", base, "0800", time.Date(2026, 1, 5, 8, 0, 0, 0, time.UTC), false}, {"midnight", base, "00:00", time.Date(2026, 1, 5, 0, 0, 0, 0, time.UTC), false}, {"end of day", base, "23:59", time.Date(2026, 1, 5, 23, 59, 0, 0, time.UTC), false}, {"invalid hour", base, "25:00", time.Time{}, true}, {"invalid minute", base, "12:65", time.Time{}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := parser.parseTimeOfDay(tt.date, tt.timeStr) if tt.wantErr { if err == nil { t.Error("Expected error but got none") } return } if err != nil { t.Fatalf("Unexpected error: %v", err) } if !result.Equal(tt.expected) { t.Errorf("Expected %v, got %v", tt.expected, result) } }) } } func TestParseDateWithTime(t *testing.T) { base := time.Date(2026, 1, 5, 12, 0, 0, 0, time.UTC) // Monday parser := NewDateParser(base, time.Monday) tests := []struct { name string input string expected time.Time }{ {"mon:15:35", "mon:15:35", time.Date(2026, 1, 12, 15, 35, 0, 0, time.UTC)}, // Next Monday at 15:35 {"tomorrow:0800", "tomorrow:0800", time.Date(2026, 1, 6, 8, 0, 0, 0, time.UTC)}, {"21jan:1430", "21jan:1430", time.Date(2026, 1, 21, 14, 30, 0, 0, time.UTC)}, {"just time 15:35", "15:35", time.Date(2026, 1, 5, 15, 35, 0, 0, time.UTC)}, // Today at 15:35 } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := parser.ParseDate(tt.input) if err != nil { t.Fatalf("Failed to parse date with time: %v", err) } if !result.Equal(tt.expected) { t.Errorf("Expected %v, got %v", tt.expected, result) } }) } } func TestSplitDateTime(t *testing.T) { base := time.Date(2026, 1, 5, 12, 0, 0, 0, time.UTC) parser := NewDateParser(base, time.Monday) tests := []struct { name string input string wantDate string wantTime string wantHas bool }{ {"plain weekday", "mon", "mon", "", false}, {"plain date", "2026-01-15", "2026-01-15", "", false}, {"weekday+HHMM", "mon:0800", "mon", "0800", true}, {"weekday+HH:MM", "mon:15:35", "mon", "15:35", true}, {"date+HHMM", "21jan:1430", "21jan", "1430", true}, {"just time HH:MM", "15:35", "", "15:35", true}, {"tomorrow+time", "tomorrow:0800", "tomorrow", "0800", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dateStr, timeStr, hasTime := parser.splitDateTime(tt.input) if hasTime != tt.wantHas { t.Errorf("hasTime = %v, want %v", hasTime, tt.wantHas) } if hasTime { if dateStr != tt.wantDate { t.Errorf("dateStr = %q, want %q", dateStr, tt.wantDate) } if timeStr != tt.wantTime { t.Errorf("timeStr = %q, want %q", timeStr, tt.wantTime) } } }) } } func TestParseISODate(t *testing.T) { // Use a non-UTC timezone to verify ISO dates respect the parser's location loc := time.FixedZone("UTC+10", 10*60*60) base := time.Date(2026, 1, 5, 12, 0, 0, 0, loc) parser := NewDateParser(base, time.Monday) result, err := parser.ParseDate("2026-02-20") if err != nil { t.Fatalf("Failed to parse ISO date: %v", err) } expected := time.Date(2026, 2, 20, 0, 0, 0, 0, loc) if !result.Equal(expected) { t.Errorf("Expected %v, got %v", expected, result) } if result.Location() != loc { t.Errorf("Expected location %v, got %v", loc, result.Location()) } } func TestParseDateInvalid(t *testing.T) { base := time.Date(2026, 1, 5, 12, 0, 0, 0, time.UTC) parser := NewDateParser(base, time.Monday) invalids := []string{ "notadate", "xyz123", "", "32jan", "feb30", } for _, input := range invalids { t.Run(input, func(t *testing.T) { _, err := parser.ParseDate(input) if err == nil && input != "" { // Some of these might parse as durations or keywords // but truly invalid ones should error t.Logf("ParseDate(%q) did not error (may be valid as keyword/duration)", input) } }) } } func TestNextWeekday_Exhaustive(t *testing.T) { // Test all 7 starting days × 7 target days // Mon Jan 5 2026 monday := time.Date(2026, 1, 5, 12, 0, 0, 0, time.UTC) allDays := []time.Weekday{ time.Sunday, time.Monday, time.Tuesday, time.Wednesday, time.Thursday, time.Friday, time.Saturday, } for fromOffset := 0; fromOffset < 7; fromOffset++ { from := monday.AddDate(0, 0, fromOffset) parser := NewDateParser(from, time.Monday) for _, target := range allDays { t.Run(from.Weekday().String()+"_to_"+target.String(), func(t *testing.T) { result := parser.nextWeekday(target) // Must land on the correct weekday if result.Weekday() != target { t.Errorf("weekday = %v, want %v", result.Weekday(), target) } // Must be 1-7 days in the future fromMidnight := time.Date(from.Year(), from.Month(), from.Day(), 0, 0, 0, 0, from.Location()) days := int(result.Sub(fromMidnight).Hours() / 24) if days < 1 || days > 7 { t.Errorf("days ahead = %d, want 1-7 (from %v to %v)", days, from, result) } // Same weekday should always be 7 days ahead if from.Weekday() == target && days != 7 { t.Errorf("same weekday should be 7 days, got %d", days) } }) } } } func TestExpandedDurationFormats(t *testing.T) { tests := []struct { name string input string expected time.Duration }{ {"5sec", "5sec", 5 * time.Second}, {"5seconds", "5seconds", 5 * time.Second}, {"10min", "10min", 10 * time.Minute}, {"10minutes", "10minutes", 10 * time.Minute}, {"2hrs", "2hrs", 2 * time.Hour}, {"2hours", "2hours", 2 * time.Hour}, {"3days", "3days", 3 * 24 * time.Hour}, {"2weeks", "2weeks", 2 * 7 * 24 * time.Hour}, {"1month", "1month", 30 * 24 * time.Hour}, {"1year", "1year", 365 * 24 * time.Hour}, {"hour (no number)", "hour", time.Hour}, {"day (no number)", "day", 24 * time.Hour}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := ParseRecurrencePattern(tt.input) if err != nil { t.Fatalf("Failed to parse duration: %v", err) } if result != tt.expected { t.Errorf("Expected %v, got %v", tt.expected, result) } }) } }