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") } if CurrentConditionsPayload(nil, UnitsUS) != nil { t.Fatalf("expected nil current conditions input to return nil payload") } } func TestCurrentConditionsPayloadMetricAndUS(t *testing.T) { conditions := &CurrentConditions{ TemperatureC: float64Ptr(20), ApparentTemperatureC: float64Ptr(18), DewpointC: float64Ptr(10), RelativeHumidityPercent: float64Ptr(55), WindSpeedKmh: float64Ptr(100), WindDirectionDegrees: float64Ptr(225), ConditionCode: 0, IsDay: boolPtr(true), } metricPayload := CurrentConditionsPayload(conditions, UnitsMetric) metric, ok := metricPayload.(CurrentConditionsResponse) if !ok { t.Fatalf("expected CurrentConditionsResponse metric payload, got %T", metricPayload) } assertApprox(t, metric.TemperatureC, 20, 0.0001) assertApprox(t, metric.WindSpeedKmh, 100, 0.0001) if metric.TemperatureF != nil || metric.WindSpeedMph != nil { t.Fatalf("expected US fields omitted for metric payload") } if metric.ConditionText != "Sunny" { t.Fatalf("expected condition text Sunny, got %q", metric.ConditionText) } usPayload := CurrentConditionsPayload(conditions, UnitsUS) us, ok := usPayload.(CurrentConditionsResponse) if !ok { t.Fatalf("expected CurrentConditionsResponse US payload, got %T", usPayload) } assertApprox(t, us.TemperatureF, 68, 0.0001) assertApprox(t, us.WindSpeedMph, 62.1371192237, 0.0001) if us.TemperatureC != nil || us.WindSpeedKmh != nil { t.Fatalf("expected metric fields omitted for US payload") } } func TestCurrentConditionsPayloadUsesNightConditionText(t *testing.T) { night := false payload := CurrentConditionsPayload(&CurrentConditions{ ConditionCode: 0, IsDay: &night, }, UnitsMetric) metric, ok := payload.(CurrentConditionsResponse) if !ok { t.Fatalf("expected CurrentConditionsResponse payload, got %T", payload) } if metric.ConditionText != "Clear" { t.Fatalf("expected condition text Clear, got %q", metric.ConditionText) } } func float64Ptr(v float64) *float64 { return &v } func boolPtr(v bool) *bool { 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) } }