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 describe "native_level_count/0 and native_variables/0" do test "native_level_count is 50 (full hybrid stack)" do assert HrrrNativeClient.native_level_count() == 50 end test "native_variables lists exactly the 7 native-grid fields" do vars = HrrrNativeClient.native_variables() assert length(vars) == 7 assert "TMP" in vars assert "SPFH" in vars assert "HGT" in vars assert "PRES" in vars assert "UGRD" in vars assert "VGRD" in vars assert "TKE" in vars end end describe "native_messages/0 shape" do test "emits one message per (level, variable) across the 50-level hybrid stack" do messages = HrrrNativeClient.native_messages() expected = HrrrNativeClient.native_level_count() * length(HrrrNativeClient.native_variables()) assert length(messages) == expected # Every message carries a non-empty var and a hybrid-level string. Enum.each(messages, fn %{var: var, level: level} -> assert var in HrrrNativeClient.native_variables() assert level =~ ~r/\A\d+ hybrid level\z/ end) end test "each (var, level) pair is unique" do messages = HrrrNativeClient.native_messages() unique = messages |> Enum.uniq_by(fn %{var: v, level: l} -> {v, l} end) |> length() assert unique == length(messages) end end describe "duct_messages/0 and duct_byte_ranges/1" do test "emits one message per (level, duct-var) across the 50-level stack (4 vars)" do messages = HrrrNativeClient.duct_messages() assert length(messages) == 50 * 4 # Exactly the duct-detection variables — no UGRD/VGRD/TKE. vars = messages |> Enum.map(& &1.var) |> Enum.uniq() |> Enum.sort() assert vars == ["HGT", "PRES", "SPFH", "TMP"] end test "duct_byte_ranges/1 returns integer ranges for matching idx entries" do 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: "UGRD", level: "1 hybrid level"}, %{msg: 4, offset: 3000, var: "HGT", level: "1 hybrid level"} ] ranges = HrrrNativeClient.duct_byte_ranges(idx_entries) # UGRD is not in the duct set, so only 3 ranges should come back. assert length(ranges) == 3 Enum.each(ranges, fn {s, e} -> assert is_integer(s) and is_integer(e) assert s < e end) end end end