package report import ( "strings" "testing" "time" "gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil" ) func TestDailyValidPeriod(t *testing.T) { location := mustLoadLocation(t) now := mustParse("2026-05-29T17:45:00-05:00") resolved, err := Resolve(DailyToday, ResolveRequest{Now: now, Location: location}) if err != nil { t.Fatalf("Resolve() error = %v", err) } assertPeriod(t, resolved.ValidPeriod, "2026-05-29T00:00:00-05:00", "2026-05-30T00:00:00-05:00") } func TestDailyValidPeriodCanUseExplicitDate(t *testing.T) { location := mustLoadLocation(t) now := mustParse("2026-05-29T17:45:00-05:00") date := mustParse("2026-05-31T12:00:00-05:00") resolved, err := Resolve(DailyToday, ResolveRequest{Now: now, Location: location, Date: date}) if err != nil { t.Fatalf("Resolve() error = %v", err) } assertPeriod(t, resolved.ValidPeriod, "2026-05-31T00:00:00-05:00", "2026-06-01T00:00:00-05:00") } func TestTomorrowValidPeriodFromEveningGeneration(t *testing.T) { location := mustLoadLocation(t) now := mustParse("2026-05-29T20:00:00-05:00") resolved, err := Resolve(DailyTomorrow, ResolveRequest{Now: now, Location: location}) if err != nil { t.Fatalf("Resolve() error = %v", err) } assertPeriod(t, resolved.ValidPeriod, "2026-05-30T00:00:00-05:00", "2026-05-31T00:00:00-05:00") if resolved.Definition.PromptID != "weather.daily_report" { t.Fatalf("PromptID = %q, want weather.daily_report", resolved.Definition.PromptID) } } func TestThreeDayPeriodCalculation(t *testing.T) { location := mustLoadLocation(t) now := mustParse("2026-05-29T05:00:00-05:00") resolved, err := Resolve(ThreeDay, ResolveRequest{Now: now, Location: location}) if err != nil { t.Fatalf("Resolve() error = %v", err) } assertPeriod(t, resolved.ValidPeriod, "2026-05-29T05:00:00-05:00", "2026-06-01T00:00:00-05:00") } func TestWeekendPeriodCalculation(t *testing.T) { location := mustLoadLocation(t) tests := []struct { name string now string start string end string }{ {name: "monday", now: "2026-05-25T05:00:00-05:00", start: "2026-05-30T00:00:00-05:00", end: "2026-06-01T00:00:00-05:00"}, {name: "tuesday", now: "2026-05-26T05:00:00-05:00", start: "2026-05-30T00:00:00-05:00", end: "2026-06-01T00:00:00-05:00"}, {name: "wednesday", now: "2026-05-27T05:00:00-05:00", start: "2026-05-30T00:00:00-05:00", end: "2026-06-01T00:00:00-05:00"}, {name: "thursday", now: "2026-05-28T05:00:00-05:00", start: "2026-05-30T00:00:00-05:00", end: "2026-06-01T00:00:00-05:00"}, {name: "friday before evening", now: "2026-05-29T05:00:00-05:00", start: "2026-05-29T18:00:00-05:00", end: "2026-06-01T00:00:00-05:00"}, {name: "friday after evening", now: "2026-05-29T19:30:00-05:00", start: "2026-05-29T19:30:00-05:00", end: "2026-06-01T00:00:00-05:00"}, {name: "saturday", now: "2026-05-30T08:00:00-05:00", start: "2026-05-30T08:00:00-05:00", end: "2026-06-01T00:00:00-05:00"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { resolved, err := Resolve(Weekend, ResolveRequest{Now: mustParse(tt.now), Location: location}) if err != nil { t.Fatalf("Resolve() error = %v", err) } assertPeriod(t, resolved.ValidPeriod, tt.start, tt.end) }) } } func TestWeekendSundayErrors(t *testing.T) { location := mustLoadLocation(t) _, err := Resolve(Weekend, ResolveRequest{Now: mustParse("2026-05-31T08:00:00-05:00"), Location: location}) if err == nil { t.Fatal("Resolve() error = nil, want Sunday weekend error") } if !strings.Contains(err.Error(), "Sunday") { t.Fatalf("error = %q, want Sunday context", err.Error()) } } func TestStormManualPeriodParsingAndValidation(t *testing.T) { location := mustLoadLocation(t) period, err := ParseStormPeriod("2026-05-29T18:00", "2026-05-30T06:00:00-05:00", location) if err != nil { t.Fatalf("ParseStormPeriod() error = %v", err) } assertPeriod(t, period, "2026-05-29T18:00:00-05:00", "2026-05-30T06:00:00-05:00") _, err = ParseStormPeriod("2026-05-30T06:00", "2026-05-29T18:00", location) if err == nil { t.Fatal("ParseStormPeriod() error = nil, want invalid period error") } } func TestStormResolve(t *testing.T) { location := mustLoadLocation(t) resolved, err := Resolve(Storm, ResolveRequest{ Now: mustParse("2026-05-29T12:00:00-05:00"), Location: location, StormStart: mustParse("2026-05-29T18:00:00-05:00"), StormEnd: mustParse("2026-05-30T06:00:00-05:00"), }) if err != nil { t.Fatalf("Resolve() error = %v", err) } assertPeriod(t, resolved.ValidPeriod, "2026-05-29T18:00:00-05:00", "2026-05-30T06:00:00-05:00") if resolved.Definition.ComparisonStrategy != CompareExplicitWindow { t.Fatalf("ComparisonStrategy = %q, want explicit event window", resolved.Definition.ComparisonStrategy) } } func TestMorningBatchSkipsWeekendOnSunday(t *testing.T) { location := mustLoadLocation(t) resolved, err := DefaultRegistry().BatchReports(Morning, ResolveRequest{ Now: mustParse("2026-05-31T06:00:00-05:00"), Location: location, }) if err != nil { t.Fatalf("BatchReports() error = %v", err) } ids := resolvedIDs(resolved) if strings.Join(ids, ",") != "daily_today,three_day" { t.Fatalf("ids = %v, want daily_today and three_day", ids) } } func TestEveningBatchIncludesTomorrow(t *testing.T) { location := mustLoadLocation(t) resolved, err := DefaultRegistry().BatchReports(Evening, ResolveRequest{ Now: mustParse("2026-05-29T18:00:00-05:00"), Location: location, }) if err != nil { t.Fatalf("BatchReports() error = %v", err) } ids := resolvedIDs(resolved) if strings.Join(ids, ",") != "daily_tomorrow" { t.Fatalf("ids = %v, want daily_tomorrow", ids) } } func TestRegistryLookupErrorIsActionable(t *testing.T) { _, err := DefaultRegistry().Lookup(ID("unknown")) if err == nil { t.Fatal("Lookup() error = nil, want unknown report error") } if !strings.Contains(err.Error(), `unknown report "unknown"`) { t.Fatalf("error = %q, want unknown report context", err.Error()) } } func TestRegistryDefinitionsHavePromptIDsAndComparisonStrategies(t *testing.T) { for _, definition := range DefaultRegistry().All() { if definition.PromptID == "" { t.Fatalf("%s PromptID is empty", definition.ID) } if definition.ComparisonStrategy == "" { t.Fatalf("%s ComparisonStrategy is empty", definition.ID) } } } func TestResolvedMetadata(t *testing.T) { location := mustLoadLocation(t) resolved, err := Resolve(DailyToday, ResolveRequest{Now: mustParse("2026-05-29T05:00:00-05:00"), Location: location}) if err != nil { t.Fatalf("Resolve() error = %v", err) } metadata := resolved.Metadata() if metadata.ReportID != DailyToday { t.Fatalf("ReportID = %q, want daily_today", metadata.ReportID) } if metadata.PromptID != "weather.daily_report" { t.Fatalf("PromptID = %q, want weather.daily_report", metadata.PromptID) } if !strings.Contains(metadata.RunID, "daily_today") { t.Fatalf("RunID = %q, want report id", metadata.RunID) } } func assertPeriod(t *testing.T, period timeutil.Period, wantStart string, wantEnd string) { t.Helper() if !period.IsValid() { t.Fatalf("period = %#v, want valid", period) } if period.Start.Format(time.RFC3339) != wantStart { t.Fatalf("Start = %s, want %s", period.Start.Format(time.RFC3339), wantStart) } if period.End.Format(time.RFC3339) != wantEnd { t.Fatalf("End = %s, want %s", period.End.Format(time.RFC3339), wantEnd) } } func resolvedIDs(resolved []Resolved) []string { ids := make([]string, 0, len(resolved)) for _, item := range resolved { ids = append(ids, string(item.Definition.ID)) } return ids } func mustLoadLocation(t *testing.T) *time.Location { t.Helper() location, err := time.LoadLocation("America/Chicago") if err != nil { t.Fatalf("load location: %v", err) } return location } func mustParse(value string) time.Time { parsed, err := time.Parse(time.RFC3339, value) if err != nil { panic(err) } return parsed }