package briefing import ( "encoding/json" "strings" "testing" "time" "gitea.maximumdirect.net/eric/weatherreporter/internal/module" "gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata" ) func TestSPCConvectiveDiscussionModuleOmitsBelowThresholdButOutlookRemains(t *testing.T) { registry := MustDefaultModuleRegistry() ctx := spcConvectiveDiscussionContext(2, []weatherdata.ConvectiveOutlookDiscussion{ spcDiscussion(1, "Lower risk", "General thunderstorms.", "No organized severe weather is expected.", "2026-05-29T08:30:00-05:00"), }) output, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.SPCConvectiveDiscussion}) if err != nil { t.Fatalf("BuildModule(discussion) error = %v", err) } if output != nil { t.Fatalf("discussion output = %#v, want omitted below threshold", output) } outlookOutput, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.SPCConvectiveOutlooks}) if err != nil { t.Fatalf("BuildModule(outlooks) error = %v", err) } outlookValue := moduleValue[SPCConvectiveOutlooksModule](t, outlookOutput) if !outlookValue.Checked || outlookValue.OutlookCount != 1 { t.Fatalf("outlook value = %#v, want lower-risk outlook still emitted", outlookValue) } } func TestSPCConvectiveDiscussionModuleIncludesEqualThresholdDiscussion(t *testing.T) { registry := MustDefaultModuleRegistry() ctx := spcConvectiveDiscussionContext(3, []weatherdata.ConvectiveOutlookDiscussion{ spcDiscussion(1, "Severe storms possible", "Scattered severe storms are possible.", "A few storms may become severe during the afternoon.", "2026-05-29T08:30:00-05:00"), }) output, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.SPCConvectiveDiscussion}) if err != nil { t.Fatalf("BuildModule() error = %v", err) } if output == nil || output.ID != module.SPCConvectiveDiscussion || output.StanzaName != "spc_convective_discussion" { t.Fatalf("output = %#v, want spc convective discussion stanza", output) } value := moduleValue[SPCConvectiveDiscussionModule](t, output) if value.IncludedBecause != "severity_rank >= 3" || len(value.Discussions) != 1 { t.Fatalf("value = %#v, want threshold reason and one discussion", value) } discussion := value.Discussions[0] if discussion.Day != 1 || discussion.Headline != "Severe storms possible" || discussion.Summary == "" || discussion.Discussion == "" { t.Fatalf("discussion = %#v, want prompt-facing discussion fields", discussion) } if discussion.UpdatedAt != "2026-05-29 at 8:30 AM" { t.Fatalf("UpdatedAt = %q, want friendly local time", discussion.UpdatedAt) } data, err := json.Marshal(output.Value) if err != nil { t.Fatalf("Marshal() error = %v", err) } text := string(data) for _, field := range []string{"included_because", "discussions", "headline", "summary", "discussion", "updated_at"} { if !strings.Contains(text, field) { t.Fatalf("json = %s, want field %s", text, field) } } } func TestSPCConvectiveDiscussionModuleIncludesAboveThresholdDiscussion(t *testing.T) { registry := MustDefaultModuleRegistry() ctx := spcConvectiveDiscussionContext(4, []weatherdata.ConvectiveOutlookDiscussion{ spcDiscussion(1, "Enhanced severe risk", "Numerous severe storms are possible.", "Severe storms may produce damaging winds.", "2026-05-29T09:15:00-05:00"), }) output, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.SPCConvectiveDiscussion}) if err != nil { t.Fatalf("BuildModule() error = %v", err) } value := moduleValue[SPCConvectiveDiscussionModule](t, output) if len(value.Discussions) != 1 || value.Discussions[0].Headline != "Enhanced severe risk" { t.Fatalf("value = %#v, want above-threshold discussion", value) } } func TestSPCConvectiveDiscussionModuleOmitsMissingDiscussionText(t *testing.T) { registry := MustDefaultModuleRegistry() ctx := spcConvectiveDiscussionContext(3, []weatherdata.ConvectiveOutlookDiscussion{ {Day: 1, Headline: "Severe storms possible", Summary: "Scattered severe storms are possible.", UpdatedAt: ptrModuleTime("2026-05-29T08:30:00-05:00")}, }) output, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.SPCConvectiveDiscussion}) if err != nil { t.Fatalf("BuildModule() error = %v", err) } if output != nil { t.Fatalf("output = %#v, want omitted when discussion text is missing", output) } } func TestSPCConvectiveDiscussionModuleOmitsMissingSource(t *testing.T) { registry := MustDefaultModuleRegistry() ctx := testModuleContext() ctx.Collected.SPCConvectiveOutlooks = nil ctx.Derived.SPCConvectiveOutlooks = nil ctx.Derived.SPCConvectiveDiscussions = nil output, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.SPCConvectiveDiscussion}) if err != nil { t.Fatalf("BuildModule() error = %v", err) } if output != nil { t.Fatalf("output = %#v, want omitted for missing optional source", output) } } func spcConvectiveDiscussionContext(rank int, discussions []weatherdata.ConvectiveOutlookDiscussion) ModuleContext { ctx := testModuleContext() outlook := weatherdata.ConvectiveOutlook{ ID: "day1-categorical", Day: 1, OutlookType: "categorical", Label: "SLGT", LabelText: "Slight Risk", SeverityRank: &rank, ValidFrom: mustParseModuleTime("2026-05-29T11:00:00-05:00"), ValidTo: mustParseModuleTime("2026-05-30T07:00:00-05:00"), } ctx.Collected.SPCConvectiveOutlooks = &weatherdata.ConvectiveOutlookRun{ AsOf: ptrModuleTime("2026-05-29T08:00:00-05:00"), IssuedAt: ptrModuleTime("2026-05-29T07:45:00-05:00"), Outlooks: []weatherdata.ConvectiveOutlook{outlook}, } ctx.Derived.SPCConvectiveOutlooks = []weatherdata.ConvectiveOutlook{outlook} ctx.Derived.SPCConvectiveDiscussions = discussions return ctx } func spcDiscussion(day int, headline string, summary string, discussion string, updatedAt string) weatherdata.ConvectiveOutlookDiscussion { return weatherdata.ConvectiveOutlookDiscussion{ Day: day, Headline: headline, Summary: summary, Discussion: discussion, UpdatedAt: ptrModuleTime(updatedAt), } } func ptrModuleTime(value string) *time.Time { parsed := mustParseModuleTime(value) return &parsed }