defmodule Microwaveprop.Weather.HrdpsClientTest do use ExUnit.Case, async: false alias Microwaveprop.Weather.HrdpsClient describe "nearest_hrdps_cycle/1" do test "snaps to the start of the current 6-hour cycle" do assert HrdpsClient.nearest_hrdps_cycle(~U[2026-04-29 13:45:00Z]) == ~U[2026-04-29 12:00:00Z] assert HrdpsClient.nearest_hrdps_cycle(~U[2026-04-29 19:00:00Z]) == ~U[2026-04-29 18:00:00Z] assert HrdpsClient.nearest_hrdps_cycle(~U[2026-04-29 00:30:00Z]) == ~U[2026-04-29 00:00:00Z] end test "rounds DOWN" do assert HrdpsClient.nearest_hrdps_cycle(~U[2026-04-29 11:59:00Z]) == ~U[2026-04-29 06:00:00Z] end test "stays same when exactly on a cycle boundary" do assert HrdpsClient.nearest_hrdps_cycle(~U[2026-04-29 12:00:00Z]) == ~U[2026-04-29 12:00:00Z] end end describe "grib_url/3" do test "builds surface 2m temperature URL with date-prefixed datamart path" do assert HrdpsClient.grib_url(:tmp_2m, ~U[2026-04-29 12:00:00Z], 0) == "https://dd.weather.gc.ca/20260429/WXO-DD/model_hrdps/continental/2.5km/12/000/" <> "20260429T12Z_MSC_HRDPS_TMP_AGL-2m_RLatLon0.0225_PT000H.grib2" end test "pads forecast hour to three digits" do assert String.contains?(HrdpsClient.grib_url(:tmp_2m, ~U[2026-04-29 12:00:00Z], 24), "PT024H.grib2") end test "uses MSC level slug for pressure-level temperature" do assert String.contains?(HrdpsClient.grib_url(:tmp_850mb, ~U[2026-04-29 12:00:00Z], 0), "MSC_HRDPS_TMP_ISBL_0850_") end test "raises on unknown variable" do assert_raise ArgumentError, ~r/unknown HRDPS variable/, fn -> HrdpsClient.grib_url(:not_a_real_var, ~U[2026-04-29 12:00:00Z], 0) end end end describe "variables/0" do test "covers the surface set" do vars = HrdpsClient.variables() for v <- [:tmp_2m, :depr_2m, :pres_sfc, :ugrd_10m, :vgrd_10m, :tcdc_sfc, :hpbl_sfc] do assert v in vars end end test "covers the grid pressure-level set" do vars = HrdpsClient.variables() for var_kind <- [:tmp, :depr, :hgt], level <- [1000, 950, 900, 850, 800, 750, 700] do assert String.to_existing_atom("#{var_kind}_#{level}mb") in vars end end end describe "concat_grib_binaries/1" do test "byte-concatenates GRIB2 records" do a = <<"GRIB", 1::24, 2::8, 0::8*100>> b = <<"GRIB", 1::24, 2::8, 1::8*200>> assert HrdpsClient.concat_grib_binaries([a, b]) == a <> b end test "returns empty binary for empty list" do assert HrdpsClient.concat_grib_binaries([]) == <<>> end end describe "build_profile_from_extracted/1" do test "translates extracted data into profile shape" do extracted = %{ "TMP:2 m above ground" => 285.15, "DPT:2 m above ground" => 280.15, "PRES:surface" => 100_000.0, "HPBL:surface" => 800.0, "UGRD:10 m above ground" => 3.0, "VGRD:10 m above ground" => 1.0, "TCDC:surface" => 50.0, "TMP:850 mb" => 275.0, "DPT:850 mb" => 270.0, "HGT:850 mb" => 1500.0 } profile = HrdpsClient.build_profile_from_extracted(extracted) assert_in_delta profile.surface_temp_c, 12.0, 0.01 assert_in_delta profile.surface_dewpoint_c, 7.0, 0.01 assert profile.surface_pressure_mb == 1000.0 assert profile.hpbl_m == 800.0 assert profile.cloud_cover_pct == 50.0 end end describe "build_profile_from_extracted/1 — DEPR fallback" do test "derives surface dewpoint from TMP - DEPR when DPT is missing" do extracted = %{ "TMP:2 m above ground" => 290.0, "DEPR:2 m above ground" => 5.0, "PRES:surface" => 100_000.0 } profile = HrdpsClient.build_profile_from_extracted(extracted) assert_in_delta profile.surface_temp_c, 16.85, 0.01 # 290 - 5 = 285 K = 11.85 C assert_in_delta profile.surface_dewpoint_c, 11.85, 0.01 end test "uses TCDC entire-atmosphere when surface variant is missing" do extracted = %{ "TMP:2 m above ground" => 280.0, "DPT:2 m above ground" => 275.0, "PRES:surface" => 100_000.0, "TCDC:entire atmosphere" => 90.0 } profile = HrdpsClient.build_profile_from_extracted(extracted) assert profile.cloud_cover_pct == 90.0 end test "returns nil surface scalars when nothing surface-level is in the input" do profile = HrdpsClient.build_profile_from_extracted(%{}) assert profile.surface_temp_c == nil assert profile.surface_dewpoint_c == nil assert profile.surface_pressure_mb == nil assert profile.profile == [] end test "skips a pressure level that is missing HGT" do extracted = %{ "TMP:2 m above ground" => 290.0, "DPT:2 m above ground" => 285.0, "PRES:surface" => 100_000.0, # Level 850 mb has TMP+DPT but no HGT — skipped. "TMP:850 mb" => 275.0, "DPT:850 mb" => 270.0, # Level 700 mb is fully populated. "TMP:700 mb" => 265.0, "DPT:700 mb" => 260.0, "HGT:700 mb" => 3000.0 } profile = HrdpsClient.build_profile_from_extracted(extracted) assert length(profile.profile) == 1 assert hd(profile.profile)["pres"] == 700.0 end end describe "cycle_available?/1 (injected http_head)" do setup do prev = Application.get_env(:microwaveprop, :hrdps_http_head) on_exit(fn -> if prev, do: Application.put_env(:microwaveprop, :hrdps_http_head, prev), else: Application.delete_env(:microwaveprop, :hrdps_http_head) end) :ok end test "returns true when head returns 200" do Application.put_env(:microwaveprop, :hrdps_http_head, fn _url, _opts -> {:ok, %{status: 200}} end) assert HrdpsClient.cycle_available?(~U[2026-04-29 12:00:00Z]) end test "returns false when head returns non-200" do Application.put_env(:microwaveprop, :hrdps_http_head, fn _url, _opts -> {:ok, %{status: 404}} end) refute HrdpsClient.cycle_available?(~U[2026-04-29 12:00:00Z]) end test "returns false on transport error" do Application.put_env(:microwaveprop, :hrdps_http_head, fn _url, _opts -> {:error, :timeout} end) refute HrdpsClient.cycle_available?(~U[2026-04-29 12:00:00Z]) end end describe "fetch_grid/3 with injected http_get" do setup do prev_get = Application.get_env(:microwaveprop, :hrdps_http_get) prev_head = Application.get_env(:microwaveprop, :hrdps_http_head) Application.put_env(:microwaveprop, :hrdps_http_head, fn _url, _opts -> {:ok, %{status: 200}} end) on_exit(fn -> if prev_get, do: Application.put_env(:microwaveprop, :hrdps_http_get, prev_get), else: Application.delete_env(:microwaveprop, :hrdps_http_get) if prev_head, do: Application.put_env(:microwaveprop, :hrdps_http_head, prev_head), else: Application.delete_env(:microwaveprop, :hrdps_http_head) end) :ok end test "returns error when variable fetch fails" do call_count = :counters.new(1, [:atomics]) Application.put_env(:microwaveprop, :hrdps_http_get, fn _url, _opts -> :counters.add(call_count, 1, 1) {:error, :timeout} end) assert {:error, _} = HrdpsClient.fetch_grid([{33.0, -97.0}], ~U[2026-04-29 12:00:00Z]) end end end