package jupiter import ( "encoding/json" "math" "os" "testing" "time" "b612.me/astro/basic" ) type galileanPublicEventBaselineRecord struct { Label string `json:"label"` Satellite int `json:"satellite"` Type string `json:"type"` StartUTC string `json:"start_utc"` StartDurationMinutes float64 `json:"start_duration_minutes"` EndUTC string `json:"end_utc"` EndDurationMinutes float64 `json:"end_duration_minutes"` } func TestGalileanPhenomenonEventWrappersMatchBasic(t *testing.T) { records := loadGalileanPublicEventBaseline(t) loc := time.FixedZone("UTC+8", 8*3600) for _, record := range records { startUTC := mustParseGalileanEventTime(t, record.StartUTC) endUTC := mustParseGalileanEventTime(t, record.EndUTC) queryBefore := startUTC.Add(-12 * time.Hour).In(loc) queryAfter := endUTC.Add(12 * time.Hour).In(loc) queryMid := startUTC.Add(endUTC.Sub(startUTC) / 2).In(loc) phenomenonType := GalileanPhenomenonType(record.Type) assertGalileanWrapperMatchesBasic( t, record.Label+" next", NextGalileanPhenomenonEvent(queryBefore, record.Satellite, phenomenonType), basic.NextJupiterGalileanPhenomenonEvent(basic.Date2JDE(queryBefore.UTC()), record.Satellite, basic.JupiterGalileanPhenomenonType(phenomenonType)), loc, ) assertGalileanWrapperMatchesBasic( t, record.Label+" last", LastGalileanPhenomenonEvent(queryAfter, record.Satellite, phenomenonType), basic.LastJupiterGalileanPhenomenonEvent(basic.Date2JDE(queryAfter.UTC()), record.Satellite, basic.JupiterGalileanPhenomenonType(phenomenonType)), loc, ) assertGalileanWrapperMatchesBasic( t, record.Label+" closest", ClosestGalileanPhenomenonEvent(queryMid, record.Satellite, phenomenonType), basic.ClosestJupiterGalileanPhenomenonEvent(basic.Date2JDE(queryMid.UTC()), record.Satellite, basic.JupiterGalileanPhenomenonType(phenomenonType)), loc, ) } } func assertGalileanWrapperMatchesBasic( t *testing.T, name string, got GalileanPhenomenonEvent, want basic.JupiterGalileanPhenomenonEvent, loc *time.Location, ) { t.Helper() if got.Valid != want.Valid { t.Fatalf("%s valid mismatch: got %v want %v", name, got.Valid, want.Valid) } if !got.Valid { return } if got.Start.Location() != loc || got.Greatest.Location() != loc || got.End.Location() != loc { t.Fatalf("%s timezone mismatch", name) } wantStart := basic.JDE2DateByZone(want.Start, loc, false) wantGreatest := basic.JDE2DateByZone(want.Greatest, loc, false) wantEnd := basic.JDE2DateByZone(want.End, loc, false) if !got.Start.Equal(wantStart) || !got.Greatest.Equal(wantGreatest) || !got.End.Equal(wantEnd) { t.Fatalf( "%s time mismatch: got [%s %s %s] want [%s %s %s]", name, got.Start.Format(time.RFC3339Nano), got.Greatest.Format(time.RFC3339Nano), got.End.Format(time.RFC3339Nano), wantStart.Format(time.RFC3339Nano), wantGreatest.Format(time.RFC3339Nano), wantEnd.Format(time.RFC3339Nano), ) } if got.Duration != got.End.Sub(got.Start) { t.Fatalf("%s duration mismatch: got %s want %s", name, got.Duration, got.End.Sub(got.Start)) } if got.Satellite != want.Satellite || string(got.Type) != string(want.Type) { t.Fatalf("%s id/type mismatch", name) } assertSameGalileanPhenomenon(t, name+" greatest", got.GreatestState, want.GreatestPhenomenon) } func assertSameGalileanPhenomenon(t *testing.T, name string, got GalileanSatellitePhenomenon, want basic.JupiterGalileanPhenomenon) { t.Helper() if got.Transit != want.Transit || got.Occultation != want.Occultation || got.Eclipse != want.Eclipse || got.ShadowTransit != want.ShadowTransit { t.Fatalf("%s flag mismatch", name) } gotFloats := []float64{got.ShadowOffsetXArcsec, got.ShadowOffsetYArcsec, got.ShadowOffsetXJupiterR, got.ShadowOffsetYJupiterR} wantFloats := []float64{want.ShadowOffsetXArcsec, want.ShadowOffsetYArcsec, want.ShadowOffsetXJupiterRadii, want.ShadowOffsetYJupiterRadii} for i := range gotFloats { if math.Float64bits(gotFloats[i]) != math.Float64bits(wantFloats[i]) { t.Fatalf("%s shadow field %d mismatch: got %.18f want %.18f", name, i, gotFloats[i], wantFloats[i]) } } } func loadGalileanPublicEventBaseline(t *testing.T) []galileanPublicEventBaselineRecord { t.Helper() data, err := os.ReadFile("testdata/galilean_events_imcce_2026.json") if err != nil { t.Fatal(err) } var records []galileanPublicEventBaselineRecord if err := json.Unmarshal(data, &records); err != nil { t.Fatal(err) } if len(records) == 0 { t.Fatal("empty galilean public event baseline") } return records } func mustParseGalileanEventTime(t *testing.T, value string) time.Time { t.Helper() date, err := time.Parse(time.RFC3339Nano, value) if err != nil { t.Fatalf("parse %q: %v", value, err) } return date }