package changes import ( "strings" "testing" "time" "gitea.maximumdirect.net/eric/weatherreporter/internal/module" "gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil" ) func TestCompareDailyNoMeaningfulChanges(t *testing.T) { previous := dailySnapshot(t, 60, 70, 30, "8 AM", nil, false) current := dailySnapshot(t, 61, 71, 35, "8:30 AM", nil, false) changes, err := CompareDaily(previous, current, testThresholds()) if err != nil { t.Fatalf("CompareDaily() error = %v", err) } if len(changes) != 0 { t.Fatalf("changes = %#v, want none", changes) } } func TestCompareDailyTemperatureThreshold(t *testing.T) { previous := dailySnapshot(t, 50, 70, 10, "8 AM", nil, false) current := dailySnapshot(t, 58, 79, 10, "8 AM", nil, false) changes, err := CompareDaily(previous, current, testThresholds()) if err != nil { t.Fatalf("CompareDaily() error = %v", err) } if countType(changes, "temperature_shift") != 2 { t.Fatalf("changes = %#v, want low and high temperature changes", changes) } } func TestCompareDailyPrecipTimingShift(t *testing.T) { previous := dailySnapshot(t, 60, 70, 60, "8 AM", nil, false) current := dailySnapshot(t, 60, 70, 60, "11 AM", nil, false) changes, err := CompareDaily(previous, current, testThresholds()) if err != nil { t.Fatalf("CompareDaily() error = %v", err) } if countType(changes, "precip_timing_shift") != 1 { t.Fatalf("changes = %#v, want timing shift", changes) } } func TestCompareDailyAlertAddedAndRemoved(t *testing.T) { previous := dailySnapshot(t, 60, 70, 10, "8 AM", []string{"Wind Advisory"}, false) current := dailySnapshot(t, 60, 70, 10, "8 AM", []string{"Flood Watch"}, false) changes, err := CompareDaily(previous, current, testThresholds()) if err != nil { t.Fatalf("CompareDaily() error = %v", err) } if countType(changes, "alert_added") != 1 || countType(changes, "alert_removed") != 1 { t.Fatalf("changes = %#v, want one alert added and one removed", changes) } } func TestCompareDailyIndicatorChange(t *testing.T) { previous := dailySnapshot(t, 60, 70, 10, "8 AM", nil, false) current := dailySnapshot(t, 60, 70, 10, "8 AM", nil, true) changes, err := CompareDaily(previous, current, testThresholds()) if err != nil { t.Fatalf("CompareDaily() error = %v", err) } if countType(changes, "snow_risk_change") != 1 { t.Fatalf("changes = %#v, want snow risk change", changes) } } func TestCompareDailyRequiresComparisonStanzas(t *testing.T) { _, err := CompareDaily(snapshot(t), dailySnapshot(t, 60, 70, 10, "8 AM", nil, false), testThresholds()) if err == nil { t.Fatal("CompareDaily() error = nil, want missing stanza error") } if !strings.Contains(err.Error(), "derived_daily_summary") { t.Fatalf("error = %q, want derived_daily_summary context", err.Error()) } } func dailySnapshot(t *testing.T, low int, high int, precip int, precipTime string, alerts []string, snow bool) module.Snapshot { t.Helper() relevant := make([]alertSummaryStanza, 0, len(alerts)) for _, alert := range alerts { relevant = append(relevant, alertSummaryStanza{Event: alert}) } return snapshot(t, module.Output{ID: module.DerivedDailySummary, StanzaName: "derived_daily_summary", Value: dailySummaryStanza{ Date: "2026-05-29", HighTempF: &high, LowTempF: &low, MaxPopPercent: &precip, }}, module.Output{ID: module.DerivedDaypartSummaries, StanzaName: "derived_daypart_summaries", Value: map[string]daypartSummaryStanza{ "morning": {Period: period("2026-05-29T06:00:00Z", "2026-05-29T10:00:00Z"), TempRangeF: "60-70", Snow: snow}, }}, module.Output{ID: module.PrecipTiming, StanzaName: "precip_timing", Value: precipTimingStanza{MaxPopPercent: &precip, MaxPopTime: precipTime}}, module.Output{ID: module.AlertDigest, StanzaName: "alert_digest", Value: alertDigestStanza{Relevant: relevant}}, ) } func snapshot(t *testing.T, outputs ...module.Output) module.Snapshot { t.Helper() snapshot, err := module.NewSnapshot(outputs) if err != nil { t.Fatalf("NewSnapshot() error = %v", err) } return snapshot } func period(start string, end string) timeutil.Period { return timeutil.Period{Start: at(start), End: at(end)} } func testThresholds() Thresholds { return Thresholds{ TemperatureDegrees: 5, PrecipProbabilityPoints: 20, WindGustMilesPerHour: 10, PrecipTimingShiftMinutes: 120, } } func countType(changes []Change, changeType string) int { var count int for _, change := range changes { if change.Type == changeType { count++ } } return count } func at(value string) time.Time { parsed, err := time.Parse(time.RFC3339, value) if err != nil { panic(err) } return parsed }