package core import ( "math" "testing" "time" "gitea.maximumdirect.net/ejr/weatherfeeder/model" ) func TestObservationPayloadUS(t *testing.T) { obs := &model.WeatherObservation{ StationID: "KSTL", Timestamp: time.Date(2026, 3, 20, 0, 0, 0, 0, time.UTC), ConditionCode: 2, TemperatureC: float64Ptr(20), DewpointC: float64Ptr(10), WindSpeedKmh: float64Ptr(100), WindGustKmh: float64Ptr(80), BarometricPressurePa: float64Ptr(101325), VisibilityMeters: float64Ptr(1609.344), ApparentTemperatureC: float64Ptr(25), RelativeHumidityPercent: float64Ptr(50), } payload := ObservationPayload(obs, UnitsUS) converted, ok := payload.(WeatherObservationUS) if !ok { t.Fatalf("expected WeatherObservationUS payload, got %T", payload) } assertApprox(t, converted.TemperatureF, 68.0, 0.0001) assertApprox(t, converted.WindSpeedMph, 62.1371192237, 0.0001) assertApprox(t, converted.BarometricPressureInHg, 29.9212524019, 0.0001) assertApprox(t, converted.VisibilityMiles, 1.0, 0.0001) if converted.TemperatureF == nil || converted.DewpointF == nil || converted.ApparentTemperatureF == nil { t.Fatalf("expected converted Fahrenheit fields to be populated") } } func TestForecastPayloadUS(t *testing.T) { issuedAt := time.Date(2026, 3, 20, 12, 0, 0, 0, time.UTC) updatedAt := issuedAt.Add(1 * time.Hour) run := &model.WeatherForecastRun{ LocationID: "stl", IssuedAt: issuedAt, UpdatedAt: &updatedAt, Product: model.ForecastProductHourly, ElevationMeters: float64Ptr(1000), Periods: []model.WeatherForecastPeriod{ { StartTime: issuedAt, EndTime: issuedAt.Add(1 * time.Hour), ConditionCode: 63, TemperatureC: float64Ptr(0), TemperatureCMin: float64Ptr(-5), TemperatureCMax: float64Ptr(5), WindSpeedKmh: float64Ptr(64.37376), PrecipitationAmountMm: float64Ptr(25.4), SnowfallDepthMM: float64Ptr(50.8), }, }, } payload := ForecastPayload(run, UnitsUS) converted, ok := payload.(WeatherForecastRunUS) if !ok { t.Fatalf("expected WeatherForecastRunUS payload, got %T", payload) } assertApprox(t, converted.ElevationFeet, 3280.839895, 0.0001) if len(converted.Periods) != 1 { t.Fatalf("expected 1 period, got %d", len(converted.Periods)) } period := converted.Periods[0] assertApprox(t, period.TemperatureF, 32.0, 0.0001) assertApprox(t, period.TemperatureFMin, 23.0, 0.0001) assertApprox(t, period.TemperatureFMax, 41.0, 0.0001) assertApprox(t, period.WindSpeedMph, 40.0, 0.0001) assertApprox(t, period.PrecipitationAmountIn, 1.0, 0.0001) assertApprox(t, period.SnowfallDepthIn, 2.0, 0.0001) } func TestMetricPassthroughAndNilHandling(t *testing.T) { obs := &model.WeatherObservation{} metric := ObservationPayload(obs, UnitsMetric) metricObs, ok := metric.(*model.WeatherObservation) if !ok { t.Fatalf("expected metric payload to remain model type, got %T", metric) } if metricObs != obs { t.Fatalf("expected metric payload to be original pointer") } if ObservationPayload(nil, UnitsUS) != nil { t.Fatalf("expected nil observation input to return nil payload") } if ForecastPayload(nil, UnitsUS) != nil { t.Fatalf("expected nil forecast input to return nil payload") } if AlertsPayload(nil, UnitsUS) != nil { t.Fatalf("expected nil alerts input to return nil payload") } } func float64Ptr(v float64) *float64 { return &v } func assertApprox(t *testing.T, got *float64, want, eps float64) { t.Helper() if got == nil { t.Fatalf("expected value near %f, got nil", want) } if math.Abs(*got-want) > eps { t.Fatalf("expected %f +/- %f, got %f", want, eps, *got) } }