defmodule Microwaveprop.Weather.HrrrNativeClientTest do use ExUnit.Case, async: true alias Microwaveprop.Weather.HrrrClient alias Microwaveprop.Weather.HrrrNativeClient describe "hrrr_native_url/3" do test "builds the wrfnatf00 URL for a given date and hour" do url = HrrrNativeClient.hrrr_native_url(~D[2026-04-09], 12) assert url == "https://noaa-hrrr-bdp-pds.s3.amazonaws.com/hrrr.20260409/conus/hrrr.t12z.wrfnatf00.grib2" end test "pads single-digit hours" do url = HrrrNativeClient.hrrr_native_url(~D[2026-04-09], 6) assert url =~ "hrrr.t06z." end test "supports non-zero forecast hours" do url = HrrrNativeClient.hrrr_native_url(~D[2026-04-09], 12, 3) assert url =~ "wrfnatf03.grib2" end end describe "native_messages/0" do test "lists 350 messages (7 vars × 50 levels)" do messages = HrrrNativeClient.native_messages() assert length(messages) == 350 end test "every level appears with every essential variable" do messages = HrrrNativeClient.native_messages() # Pick level 5 — should have all 7 vars level5 = Enum.filter(messages, fn %{level: l} -> l == "5 hybrid level" end) vars = level5 |> Enum.map(& &1.var) |> Enum.sort() assert vars == ~w(HGT PRES SPFH TKE TMP UGRD VGRD) end test "covers levels 1 through 50" do levels = HrrrNativeClient.native_messages() |> Enum.map(& &1.level) |> Enum.uniq() |> Enum.map(fn level_str -> [digits, _] = String.split(level_str, " ", parts: 2) String.to_integer(digits) end) |> Enum.sort() assert levels == Enum.to_list(1..50) end end describe "build_native_profile/1" do test "assembles arrays in level order and caches surface scalars" do parsed = %{ "HGT:1 hybrid level" => 8.0, "TMP:1 hybrid level" => 295.0, "SPFH:1 hybrid level" => 0.010, "PRES:1 hybrid level" => 101_000.0, "UGRD:1 hybrid level" => 2.0, "VGRD:1 hybrid level" => 1.0, "TKE:1 hybrid level" => 0.5, "HGT:2 hybrid level" => 25.0, "TMP:2 hybrid level" => 293.0, "SPFH:2 hybrid level" => 0.008, "PRES:2 hybrid level" => 99_800.0, "UGRD:2 hybrid level" => 3.0, "VGRD:2 hybrid level" => 1.5, "TKE:2 hybrid level" => 0.3, "TMP:surface" => 295.5, "SPFH:2 m above ground" => 0.011, "PRES:surface" => 101_325.0 } profile = HrrrNativeClient.build_native_profile(parsed) assert profile.level_count == 2 assert profile.heights_m == [8.0, 25.0] assert profile.temp_k == [295.0, 293.0] assert profile.spfh == [0.010, 0.008] assert profile.u_wind_ms == [2.0, 3.0] assert profile.surface_temp_k == 295.5 assert profile.surface_spfh == 0.011 assert profile.surface_pressure_pa == 101_325.0 end test "skips levels missing either HGT or TMP" do parsed = %{ "HGT:1 hybrid level" => 8.0, "TMP:1 hybrid level" => 295.0, # level 2 has HGT but no TMP → skipped "HGT:2 hybrid level" => 25.0, "SPFH:2 hybrid level" => 0.008 } profile = HrrrNativeClient.build_native_profile(parsed) assert profile.level_count == 1 assert profile.heights_m == [8.0] end test "orders levels by ascending height, not by hybrid level number" do # Pathological: level 2 happens to be lower than level 1 (terrain). parsed = %{ "HGT:1 hybrid level" => 50.0, "TMP:1 hybrid level" => 290.0, "HGT:2 hybrid level" => 30.0, "TMP:2 hybrid level" => 292.0 } profile = HrrrNativeClient.build_native_profile(parsed) assert profile.heights_m == [30.0, 50.0] assert profile.temp_k == [292.0, 290.0] end test "falls back to lowest level for surface scalars when parsed has no surface entries" do parsed = %{ "HGT:1 hybrid level" => 8.0, "TMP:1 hybrid level" => 295.0, "SPFH:1 hybrid level" => 0.010, "PRES:1 hybrid level" => 101_000.0 } profile = HrrrNativeClient.build_native_profile(parsed) assert profile.surface_temp_k == 295.0 assert profile.surface_spfh == 0.010 assert profile.surface_pressure_pa == 101_000.0 end end describe "essential_byte_ranges/1" do test "produces one range per essential message present in the idx" do # Minimal fake idx: level 1 TMP/SPFH/HGT with nothing else. idx_entries = [ %{msg: 1, offset: 0, var: "TMP", level: "1 hybrid level"}, %{msg: 2, offset: 1000, var: "SPFH", level: "1 hybrid level"}, %{msg: 3, offset: 2000, var: "HGT", level: "1 hybrid level"}, %{msg: 4, offset: 3000, var: "REFC", level: "entire atmosphere"} ] ranges = HrrrNativeClient.essential_byte_ranges(idx_entries) # Expect 3 ranges (the three essentials we actually have) assert length(ranges) == 3 # Each range is {start, end} tuples, all integers Enum.each(ranges, fn {s, e} -> assert is_integer(s) and is_integer(e) assert s < e end) end test "delegates to HrrrClient.byte_ranges_for_messages/2" do # Just check the two stay in sync by computing the same ranges # two different ways. idx_entries = [ %{msg: 1, offset: 0, var: "TMP", level: "1 hybrid level"}, %{msg: 2, offset: 5000, var: "UGRD", level: "1 hybrid level"} ] assert HrrrNativeClient.essential_byte_ranges(idx_entries) == HrrrClient.byte_ranges_for_messages(idx_entries, HrrrNativeClient.native_messages()) end end end