package reports import ( "fmt" "strings" "testing" "time" ) func TestParseDateRange(t *testing.T) { // Use a fixed time for testing testTime := time.Date(2024, time.August, 15, 10, 30, 0, 0, time.UTC) // Thursday, August 15, 2024 tests := []struct { name string input string wantErr bool validate func(t *testing.T, dr DateRange) }{ { name: "this week", input: "this week", wantErr: false, validate: func(t *testing.T, dr DateRange) { // Should be Monday Aug 12 to Sunday Aug 18 expectedStart := time.Date(2024, time.August, 12, 0, 0, 0, 0, time.UTC) expectedEnd := time.Date(2024, time.August, 18, 23, 59, 59, 999999999, time.UTC) if !dr.Start.Equal(expectedStart) { t.Errorf("Start = %v, want %v", dr.Start, expectedStart) } if !dr.End.Equal(expectedEnd) { t.Errorf("End = %v, want %v", dr.End, expectedEnd) } }, }, { name: "this month", input: "this month", wantErr: false, validate: func(t *testing.T, dr DateRange) { // Should be Aug 1 to Aug 31 expectedStart := time.Date(2024, time.August, 1, 0, 0, 0, 0, time.UTC) expectedEnd := time.Date(2024, time.August, 31, 23, 59, 59, 999999999, time.UTC) if !dr.Start.Equal(expectedStart) { t.Errorf("Start = %v, want %v", dr.Start, expectedStart) } if !dr.End.Equal(expectedEnd) { t.Errorf("End = %v, want %v", dr.End, expectedEnd) } }, }, { name: "month name - current month", input: "august", wantErr: false, validate: func(t *testing.T, dr DateRange) { // Should be August 2024 (current year since we're in August) expectedStart := time.Date(2024, time.August, 1, 0, 0, 0, 0, time.UTC) expectedEnd := time.Date(2024, time.August, 31, 23, 59, 59, 999999999, time.UTC) if !dr.Start.Equal(expectedStart) { t.Errorf("Start = %v, want %v", dr.Start, expectedStart) } if !dr.End.Equal(expectedEnd) { t.Errorf("End = %v, want %v", dr.End, expectedEnd) } }, }, { name: "month name - past month this year", input: "july", wantErr: false, validate: func(t *testing.T, dr DateRange) { // Should be July 2024 (current year since July already passed) expectedStart := time.Date(2024, time.July, 1, 0, 0, 0, 0, time.UTC) expectedEnd := time.Date(2024, time.July, 31, 23, 59, 59, 999999999, time.UTC) if !dr.Start.Equal(expectedStart) { t.Errorf("Start = %v, want %v", dr.Start, expectedStart) } if !dr.End.Equal(expectedEnd) { t.Errorf("End = %v, want %v", dr.End, expectedEnd) } }, }, { name: "month name - future month last year", input: "december", wantErr: false, validate: func(t *testing.T, dr DateRange) { // Should be December 2023 (last year since December hasn't come yet) expectedStart := time.Date(2023, time.December, 1, 0, 0, 0, 0, time.UTC) expectedEnd := time.Date(2023, time.December, 31, 23, 59, 59, 999999999, time.UTC) if !dr.Start.Equal(expectedStart) { t.Errorf("Start = %v, want %v", dr.Start, expectedStart) } if !dr.End.Equal(expectedEnd) { t.Errorf("End = %v, want %v", dr.End, expectedEnd) } }, }, { name: "case insensitive month name", input: "FEBRUARY", wantErr: false, validate: func(t *testing.T, dr DateRange) { // Should be February 2024 (current year since February already passed) expectedStart := time.Date(2024, time.February, 1, 0, 0, 0, 0, time.UTC) expectedEnd := time.Date(2024, time.February, 29, 23, 59, 59, 999999999, time.UTC) // 2024 is a leap year if !dr.Start.Equal(expectedStart) { t.Errorf("Start = %v, want %v", dr.Start, expectedStart) } if !dr.End.Equal(expectedEnd) { t.Errorf("End = %v, want %v", dr.End, expectedEnd) } }, }, { name: "short month name", input: "feb", wantErr: false, validate: func(t *testing.T, dr DateRange) { // Should be February 2024 expectedStart := time.Date(2024, time.February, 1, 0, 0, 0, 0, time.UTC) expectedEnd := time.Date(2024, time.February, 29, 23, 59, 59, 999999999, time.UTC) // 2024 is a leap year if !dr.Start.Equal(expectedStart) { t.Errorf("Start = %v, want %v", dr.Start, expectedStart) } if !dr.End.Equal(expectedEnd) { t.Errorf("End = %v, want %v", dr.End, expectedEnd) } }, }, { name: "month year format", input: "july 2023", wantErr: false, validate: func(t *testing.T, dr DateRange) { // Should be July 2023 expectedStart := time.Date(2023, time.July, 1, 0, 0, 0, 0, time.UTC) expectedEnd := time.Date(2023, time.July, 31, 23, 59, 59, 999999999, time.UTC) if !dr.Start.Equal(expectedStart) { t.Errorf("Start = %v, want %v", dr.Start, expectedStart) } if !dr.End.Equal(expectedEnd) { t.Errorf("End = %v, want %v", dr.End, expectedEnd) } }, }, { name: "short month year format", input: "feb 2022", wantErr: false, validate: func(t *testing.T, dr DateRange) { // Should be February 2022 expectedStart := time.Date(2022, time.February, 1, 0, 0, 0, 0, time.UTC) expectedEnd := time.Date(2022, time.February, 28, 23, 59, 59, 999999999, time.UTC) // 2022 is not a leap year if !dr.Start.Equal(expectedStart) { t.Errorf("Start = %v, want %v", dr.Start, expectedStart) } if !dr.End.Equal(expectedEnd) { t.Errorf("End = %v, want %v", dr.End, expectedEnd) } }, }, { name: "case insensitive month year", input: "MARCH 2023", wantErr: false, validate: func(t *testing.T, dr DateRange) { // Should be March 2023 expectedStart := time.Date(2023, time.March, 1, 0, 0, 0, 0, time.UTC) expectedEnd := time.Date(2023, time.March, 31, 23, 59, 59, 999999999, time.UTC) if !dr.Start.Equal(expectedStart) { t.Errorf("Start = %v, want %v", dr.Start, expectedStart) } if !dr.End.Equal(expectedEnd) { t.Errorf("End = %v, want %v", dr.End, expectedEnd) } }, }, { name: "invalid month name", input: "invalid", wantErr: true, }, { name: "invalid month year format", input: "july 23", wantErr: true, }, { name: "invalid year", input: "july abcd", wantErr: true, }, { name: "since format", input: "since 2024-07-01", wantErr: false, validate: func(t *testing.T, dr DateRange) { // Should start on July 1, 2024 with no end date expectedStart := time.Date(2024, time.July, 1, 0, 0, 0, 0, time.UTC) if !dr.Start.Equal(expectedStart) { t.Errorf("Start = %v, want %v", dr.Start, expectedStart) } if !dr.End.IsZero() { t.Errorf("End = %v, want zero time (no end date)", dr.End) } }, }, { name: "since format case insensitive", input: "SINCE 2024-12-25", wantErr: false, validate: func(t *testing.T, dr DateRange) { // Should start on December 25, 2024 with no end date expectedStart := time.Date(2024, time.December, 25, 0, 0, 0, 0, time.UTC) if !dr.Start.Equal(expectedStart) { t.Errorf("Start = %v, want %v", dr.Start, expectedStart) } if !dr.End.IsZero() { t.Errorf("End = %v, want zero time (no end date)", dr.End) } }, }, { name: "since format invalid date", input: "since 2024-13-01", wantErr: true, }, { name: "since format missing date", input: "since", wantErr: true, }, { name: "custom date range format", input: "2024-07-01 to 2024-07-31", wantErr: false, validate: func(t *testing.T, dr DateRange) { expectedStart := time.Date(2024, time.July, 1, 0, 0, 0, 0, time.UTC) expectedEnd := time.Date(2024, time.July, 31, 23, 59, 59, 999999999, time.UTC) if !dr.Start.Equal(expectedStart) { t.Errorf("Start = %v, want %v", dr.Start, expectedStart) } if !dr.End.Equal(expectedEnd) { t.Errorf("End = %v, want %v", dr.End, expectedEnd) } }, }, { name: "custom date range - same day", input: "2024-08-15 to 2024-08-15", wantErr: false, validate: func(t *testing.T, dr DateRange) { expectedStart := time.Date(2024, time.August, 15, 0, 0, 0, 0, time.UTC) expectedEnd := time.Date(2024, time.August, 15, 23, 59, 59, 999999999, time.UTC) if !dr.Start.Equal(expectedStart) { t.Errorf("Start = %v, want %v", dr.Start, expectedStart) } if !dr.End.Equal(expectedEnd) { t.Errorf("End = %v, want %v", dr.End, expectedEnd) } }, }, { name: "custom date range - invalid order", input: "2024-08-15 to 2024-08-01", wantErr: true, }, { name: "custom date range - invalid format", input: "2024-08-15 through 2024-08-31", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create a custom ParseDateRange that uses our test time testParseFunc := func(dateStr string) (DateRange, error) { dateStr = strings.TrimSpace(dateStr) // Check for predefined ranges (case-insensitive) lowerDateStr := strings.ToLower(dateStr) switch lowerDateStr { case "last week": return getLastWeek(testTime), nil case "this week": return getThisWeek(testTime), nil case "last month": return getLastMonth(testTime), nil case "this month": return getThisMonth(testTime), nil } // Check for month name patterns (case-insensitive) if dateRange, err := parseMonthName(dateStr, testTime); err == nil { return dateRange, nil } // Check for "month year" format if dateRange, err := parseMonthYear(dateStr); err == nil { return dateRange, nil } // Check for "since YYYY-MM-DD" format if strings.HasPrefix(lowerDateStr, "since ") { return parseSinceDateRange(dateStr) } // Check for custom date range format: "YYYY-MM-DD to YYYY-MM-DD" if strings.Contains(dateStr, " to ") { return parseCustomDateRange(dateStr) } return DateRange{}, fmt.Errorf("unsupported date range: %s", dateStr) } got, err := testParseFunc(tt.input) if (err != nil) != tt.wantErr { t.Errorf("ParseDateRange() error = %v, wantErr %v", err, tt.wantErr) return } if !tt.wantErr && tt.validate != nil { tt.validate(t, got) } }) } } func TestParseDateRangeDSTTransitions(t *testing.T) { // Test date range parsing during DST transitions // Use dates around US DST transitions (2nd Sunday in March, 1st Sunday in November) tests := []struct { name string input string currentTime time.Time wantStart time.Time wantEnd time.Time wantErr bool }{ { name: "this week during spring DST transition", input: "this week", currentTime: time.Date(2024, time.March, 10, 12, 0, 0, 0, time.UTC), // Sunday of DST transition week wantStart: time.Date(2024, time.March, 4, 0, 0, 0, 0, time.UTC), // Monday of that week wantEnd: time.Date(2024, time.March, 10, 23, 59, 59, 999999999, time.UTC), // Sunday 23:59:59 }, { name: "this week during fall DST transition", input: "this week", currentTime: time.Date(2024, time.November, 3, 12, 0, 0, 0, time.UTC), // Sunday of DST transition week wantStart: time.Date(2024, time.October, 28, 0, 0, 0, 0, time.UTC), // Monday of that week wantEnd: time.Date(2024, time.November, 3, 23, 59, 59, 999999999, time.UTC), // Sunday 23:59:59 }, { name: "last week before spring DST", input: "last week", currentTime: time.Date(2024, time.March, 11, 12, 0, 0, 0, time.UTC), // Monday after DST transition wantStart: time.Date(2024, time.March, 4, 0, 0, 0, 0, time.UTC), // Start of previous week wantEnd: time.Date(2024, time.March, 10, 23, 59, 59, 999999999, time.UTC), // End of previous week }, { name: "this month with DST transition", input: "this month", currentTime: time.Date(2024, time.March, 15, 12, 0, 0, 0, time.UTC), // Mid-March after DST wantStart: time.Date(2024, time.March, 1, 0, 0, 0, 0, time.UTC), wantEnd: time.Date(2024, time.March, 31, 23, 59, 59, 999999999, time.UTC), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create custom parse function with specific time testParseFunc := func(dateStr string) (DateRange, error) { dateStr = strings.TrimSpace(dateStr) lowerDateStr := strings.ToLower(dateStr) switch lowerDateStr { case "last week": return getLastWeek(tt.currentTime), nil case "this week": return getThisWeek(tt.currentTime), nil case "last month": return getLastMonth(tt.currentTime), nil case "this month": return getThisMonth(tt.currentTime), nil } return DateRange{}, fmt.Errorf("unsupported test case") } result, err := testParseFunc(tt.input) if tt.wantErr { if err == nil { t.Errorf("Expected error but got none") } return } if err != nil { t.Errorf("Unexpected error: %v", err) return } if !result.Start.Equal(tt.wantStart) { t.Errorf("Start = %v, want %v", result.Start, tt.wantStart) } if !result.End.Equal(tt.wantEnd) { t.Errorf("End = %v, want %v", result.End, tt.wantEnd) } }) } } func TestParseDateRangeTimezoneEdgeCases(t *testing.T) { // Test edge cases around timezone boundaries tests := []struct { name string input string currentTime time.Time wantStart time.Time wantEnd time.Time }{ { name: "new years boundary - this week", input: "this week", currentTime: time.Date(2025, time.January, 1, 12, 0, 0, 0, time.UTC), // Wednesday New Year's Day wantStart: time.Date(2024, time.December, 30, 0, 0, 0, 0, time.UTC), // Monday of that week (prev year) wantEnd: time.Date(2025, time.January, 5, 23, 59, 59, 999999999, time.UTC), // Sunday of that week (new year) }, { name: "month boundary - this week", input: "this week", currentTime: time.Date(2024, time.August, 1, 12, 0, 0, 0, time.UTC), // Thursday Aug 1 wantStart: time.Date(2024, time.July, 29, 0, 0, 0, 0, time.UTC), // Monday of that week (prev month) wantEnd: time.Date(2024, time.August, 4, 23, 59, 59, 999999999, time.UTC), // Sunday of that week (current month) }, { name: "leap year february", input: "february", currentTime: time.Date(2024, time.August, 15, 10, 30, 0, 0, time.UTC), // 2024 is leap year wantStart: time.Date(2024, time.February, 1, 0, 0, 0, 0, time.UTC), wantEnd: time.Date(2024, time.February, 29, 23, 59, 59, 999999999, time.UTC), // Leap day }, { name: "non-leap year february", input: "february", currentTime: time.Date(2023, time.August, 15, 10, 30, 0, 0, time.UTC), // 2023 is not leap year wantStart: time.Date(2023, time.February, 1, 0, 0, 0, 0, time.UTC), wantEnd: time.Date(2023, time.February, 28, 23, 59, 59, 999999999, time.UTC), // No leap day }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { testParseFunc := func(dateStr string) (DateRange, error) { dateStr = strings.TrimSpace(dateStr) lowerDateStr := strings.ToLower(dateStr) switch lowerDateStr { case "this week": return getThisWeek(tt.currentTime), nil case "this month": return getThisMonth(tt.currentTime), nil } // Check for month name patterns if dateRange, err := parseMonthName(dateStr, tt.currentTime); err == nil { return dateRange, nil } return DateRange{}, fmt.Errorf("unsupported test case") } result, err := testParseFunc(tt.input) if err != nil { t.Errorf("Unexpected error: %v", err) return } if !result.Start.Equal(tt.wantStart) { t.Errorf("Start = %v, want %v", result.Start, tt.wantStart) } if !result.End.Equal(tt.wantEnd) { t.Errorf("End = %v, want %v", result.End, tt.wantEnd) } }) } }