package stats import ( "context" "strings" "testing" "time" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope" ) func TestSourceKeyUsesProtocolDeviceAndSourceIP(t *testing.T) { got := SourceKey(envelope.ProtocolJT808, "13307765812", "", "115.231.168.135") if got != "JT808:13307765812@115.231.168.135" { t.Fatalf("source key = %q", got) } got = SourceKey(envelope.ProtocolGB32960, "", "iccid-1", "202.98.117.132") if got != "GB32960:iccid-1@202.98.117.132" { t.Fatalf("source key = %q", got) } } func TestUpsertSourceMileageWritesCandidateRow(t *testing.T) { exec := &recordingExec{} sample := SourceMileageSample{ VIN: "LA9GG64L7PBAF4001", StatDate: "2026-07-08", Protocol: envelope.ProtocolJT808, SourceKey: "JT808:13307765812@115.231.168.135", SourceIP: "115.231.168.135", SourceEndpoint: "115.231.168.135:20215", Phone: "13307765812", FirstTotalKM: 4100.8, LatestTotalKM: 4123.9, DailyKM: 23.1, SampleCount: 2, FirstEventTime: time.Date(2026, 7, 8, 0, 1, 0, 0, time.FixedZone("Asia/Shanghai", 8*3600)), LatestEventTime: time.Date(2026, 7, 8, 23, 59, 0, 0, time.FixedZone("Asia/Shanghai", 8*3600)), QualityStatus: QualityOK, QualityReason: "same_source_continuous", } if err := UpsertSourceMileage(context.Background(), exec, sample); err != nil { t.Fatalf("UpsertSourceMileage() error = %v", err) } if len(exec.calls) != 1 { t.Fatalf("exec calls = %d", len(exec.calls)) } sql := exec.calls[0].query for _, want := range []string{ "INSERT INTO vehicle_daily_mileage_source", "ON DUPLICATE KEY UPDATE", "daily_mileage_km = GREATEST(", "quality_status = VALUES(quality_status)", "platform_name = COALESCE(NULLIF(TRIM(VALUES(platform_name)), ''), platform_name)", } { if !strings.Contains(sql, want) { t.Fatalf("candidate upsert missing %q: %s", want, sql) } } if got := exec.calls[0].args[0]; got != "LA9GG64L7PBAF4001" { t.Fatalf("first arg = %#v", got) } } func TestUpsertSourceMileageSkipsBlankSourceIP(t *testing.T) { exec := &recordingExec{} sample := SourceMileageSample{ VIN: "LA9GG64L7PBAF4001", StatDate: "2026-07-08", Protocol: envelope.ProtocolJT808, SourceKey: "JT808:13307765812@", SourceIP: " ", Phone: "13307765812", QualityStatus: QualityOK, } if err := UpsertSourceMileage(context.Background(), exec, sample); err != nil { t.Fatalf("UpsertSourceMileage() error = %v", err) } if len(exec.calls) != 0 { t.Fatalf("exec calls = %d, want 0", len(exec.calls)) } } func TestProjectDailyMileageSelectsCandidateAndMarksSource(t *testing.T) { exec := &recordingExec{} err := ProjectDailyMileage(context.Background(), exec, "LA9GG64L7PBAF4001", "2026-07-08", envelope.ProtocolJT808) if err != nil { t.Fatalf("ProjectDailyMileage() error = %v", err) } if len(exec.calls) != 4 { t.Fatalf("exec calls = %d, want 4", len(exec.calls)) } updateSources := exec.calls[0].query projectFinal := exec.calls[1].query markSelected := exec.calls[2].query cleanupFinal := exec.calls[3].query if !strings.Contains(updateSources, "UPDATE vehicle_daily_mileage_source") || !strings.Contains(updateSources, "is_selected = 0") { t.Fatalf("first query should clear selected candidates: %s", updateSources) } for _, want := range []string{ "INSERT INTO vehicle_daily_mileage", "FROM vehicle_daily_mileage_source s", "LEFT JOIN vehicle_data_source ds", "ORDER BY COALESCE(ds.trust_priority, 100)", "s.quality_status = '" + QualityOK + "'", "s.daily_mileage_km BETWEEN 0 AND", "ds.latest_source_endpoint", } { if !strings.Contains(projectFinal, want) { t.Fatalf("project query missing %q: %s", want, projectFinal) } } if strings.Contains(projectFinal, "COALESCE(NULLIF(ds.latest_source_endpoint, ''), s.source_endpoint)") { t.Fatalf("project query should not fallback to source endpoint: %s", projectFinal) } if strings.Contains(projectFinal, ", s.source_endpoint") { t.Fatalf("project query should only project latest_source_endpoint: %s", projectFinal) } if !strings.Contains(markSelected, "UPDATE vehicle_daily_mileage_source s") || !strings.Contains(markSelected, "SET s.is_selected = 1") { t.Fatalf("mark query should flag the elected source: %s", markSelected) } if strings.Contains(markSelected, "JOIN vehicle_daily_mileage m") { t.Fatalf("mark query should not rejoin final mileage for candidate selection: %s", markSelected) } for _, want := range []string{ "JOIN (", "SELECT", "s2.source_key", "s2.quality_status = '" + QualityOK + "'", "s2.daily_mileage_km BETWEEN 0 AND", "COALESCE(ds.enabled, 1) = 1", "ORDER BY COALESCE(ds.trust_priority, 100)", "LIMIT 1", } { if !strings.Contains(markSelected, want) { t.Fatalf("mark query missing %q: %s", want, markSelected) } } if len(exec.calls[2].args) != 7 { t.Fatalf("mark query args = %d, want 7", len(exec.calls[2].args)) } if got := exec.calls[2].args[3]; got != maxSelectedDailyMileageKM { t.Fatalf("mark query max mileage arg = %#v, want %d", got, maxSelectedDailyMileageKM) } for _, want := range []string{ "DELETE FROM vehicle_daily_mileage", "NOT EXISTS (", "FROM vehicle_daily_mileage_source", "is_selected = 1", } { if !strings.Contains(cleanupFinal, want) { t.Fatalf("cleanup query missing %q: %s", want, cleanupFinal) } } if len(exec.calls[3].args) != 6 { t.Fatalf("cleanup query args = %d, want 6", len(exec.calls[3].args)) } }