defmodule Microwaveprop.Weather.UntestedFunctionsTest do use Microwaveprop.DataCase, async: true alias Microwaveprop.Repo alias Microwaveprop.Weather alias Microwaveprop.Weather.GefsProfile alias Microwaveprop.Weather.HrrrProfile alias Microwaveprop.Weather.SolarIndex alias Microwaveprop.Weather.Station defp create_station(attrs \\ %{}) do defaults = %{ station_code: "KDFW", station_type: "asos", name: "Dallas/Fort Worth Intl", lat: 32.9, lon: -97.0 } {:ok, station} = %Station{} |> Station.changeset(Map.merge(defaults, attrs)) |> Repo.insert() station end describe "upsert_solar_index/1" do test "inserts a new solar index record" do attrs = %{ date: ~D[2026-05-01], sfi: 150.0, sunspot_number: 80, ap_index: 5 } assert {:ok, %SolarIndex{} = idx} = Weather.upsert_solar_index(attrs) assert idx.sfi == 150.0 assert idx.sunspot_number == 80 assert idx.date == ~D[2026-05-01] end test "updates existing record on conflict" do Weather.upsert_solar_index(%{date: ~D[2026-05-02], sfi: 140.0, ap_index: 4}) assert {:ok, %SolarIndex{} = idx} = Weather.upsert_solar_index(%{date: ~D[2026-05-02], sfi: 145.0, ap_index: 4}) assert idx.sfi == 145.0 end end describe "station_day_pairs_covered/1" do test "empty list returns empty MapSet" do assert Weather.station_day_pairs_covered([]) == MapSet.new() end end describe "station_ids_with_surface_observations/3" do test "returns station ids that have observations in window" do st = create_station() {:ok, _obs} = Weather.upsert_surface_observation(st, %{observed_at: ~U[2026-05-01 12:00:00Z], temp_f: 75.0}) result = Weather.station_ids_with_surface_observations([st.id], ~U[2026-05-01 00:00:00Z], ~U[2026-05-02 00:00:00Z]) assert MapSet.member?(result, st.id) end test "excludes stations outside the time window" do st = create_station(%{station_code: "KLAX"}) {:ok, _obs} = Weather.upsert_surface_observation(st, %{observed_at: ~U[2026-05-01 12:00:00Z], temp_f: 70.0}) result = Weather.station_ids_with_surface_observations([st.id], ~U[2026-06-01 00:00:00Z], ~U[2026-06-02 00:00:00Z]) assert MapSet.size(result) == 0 end end describe "upsert_hrrr_profile/1" do test "inserts a new HRRR profile" do now = DateTime.truncate(DateTime.utc_now(), :second) profile_data = [%{"pres" => 1000.0, "hght" => 110, "tmpc" => 25.0, "dwpc" => 18.0}] attrs = %{ valid_time: now, run_time: now, lat: 32.9, lon: -97.0, profile: profile_data, hpbl_m: 1500.0, pwat_mm: 25.0, surface_temp_c: 25.0, surface_dewpoint_c: 18.0, surface_pressure_mb: 1013.0, surface_refractivity: 320.5, min_refractivity_gradient: -45.0, ducting_detected: false } assert {:ok, %HrrrProfile{} = profile} = Weather.upsert_hrrr_profile(attrs) assert profile.lat == 32.9 assert profile.lon == -97.0 end end describe "upsert_gefs_profile/1" do test "inserts a new GEFS profile" do now = DateTime.truncate(DateTime.utc_now(), :second) profile_data = [%{"pres" => 1000.0, "hght" => 110, "tmpc" => 25.0, "dwpc" => 18.0}] attrs = %{ valid_time: now, run_time: now, forecast_hour: 0, lat: 32.9, lon: -97.0, profile: profile_data, pwat_mm: 25.0, surface_temp_c: 25.0, surface_dewpoint_c: 18.0, surface_pressure_mb: 1013.0, surface_refractivity: 320.5, min_refractivity_gradient: -45.0, ducting_detected: false } assert {:ok, %GefsProfile{} = profile} = Weather.upsert_gefs_profile(attrs) assert profile.forecast_hour == 0 assert profile.lat == 32.9 end end describe "upsert_gefs_profiles_batch/1" do test "bulk inserts gefs profiles" do now = DateTime.truncate(DateTime.utc_now(), :second) profile_data = [%{"pres" => 1000.0, "hght" => 110, "tmpc" => 25.0, "dwpc" => 18.0}] profiles = [ %{ valid_time: now, run_time: now, forecast_hour: 0, lat: 32.9, lon: -97.0, profile: profile_data, pwat_mm: 25.0, surface_temp_c: 25.0, surface_dewpoint_c: 18.0, surface_pressure_mb: 1013.0, surface_refractivity: 320.5, min_refractivity_gradient: -45.0, ducting_detected: false } ] assert {1, _} = Weather.upsert_gefs_profiles_batch(profiles) end end describe "available_weather_valid_times/0" do test "returns a list (empty when no data)" do assert is_list(Weather.available_weather_valid_times()) end end describe "available_hrdps_valid_times/0" do test "returns a list (empty when no data)" do assert is_list(Weather.available_hrdps_valid_times()) end end describe "round_to_hrrr_grid/2" do test "snaps coordinates to HRRR 3km grid" do {lat, lon} = Weather.round_to_hrrr_grid(32.91, -97.04) assert is_float(lat) assert is_float(lon) end end describe "round_to_iemre_grid/2" do test "snaps coordinates to IEMRE 0.125 degree grid" do {lat, lon} = Weather.round_to_iemre_grid(32.91, -97.04) assert is_float(lat) assert is_float(lon) end end describe "nil guard functions" do test "hrrr_data_fully_present?/1 returns false for nil pos1" do refute Weather.hrrr_data_fully_present?(%{pos1: nil}) end test "hrrr_data_fully_present?/1 returns false for nil qso_timestamp" do refute Weather.hrrr_data_fully_present?(%{pos1: %{"lat" => 32.9, "lon" => -97.0}, qso_timestamp: nil}) end test "hrrr_for_contact/1 returns nil for nil pos1" do assert Weather.hrrr_for_contact(%{pos1: nil}) == nil end test "narr_for_contact/1 returns nil for nil pos1" do assert Weather.narr_for_contact(%{pos1: nil}) == nil end test "iemre_for_contact/1 returns nil for nil pos1" do assert Weather.iemre_for_contact(%{pos1: nil}) == nil end test "iemre_for_path/1 returns empty for nil pos1" do assert Weather.iemre_for_path(%{pos1: nil}) == [] end test "narr_profiles_for_path/1 returns empty for nil pos1" do assert Weather.narr_profiles_for_path(%{pos1: nil}) == [] end test "hrrr_profiles_for_path/1 returns empty for nil pos1" do assert Weather.hrrr_profiles_for_path(%{pos1: nil}) == [] end end describe "has_hrrr_profile?/3" do test "returns false when no profile exists" do refute Weather.has_hrrr_profile?(32.9, -97.0, DateTime.utc_now()) end end describe "has_iemre_observation?/3" do test "returns false when no observation exists" do refute Weather.has_iemre_observation?(32.9, -97.0, ~D[2026-05-01]) end end describe "purge_grid_point_profiles/0" do test "returns a count (0 when no data)" do result = Weather.purge_grid_point_profiles() assert is_integer(result) assert result >= 0 end end describe "find_or_create_station/1" do test "creates a new station when not present" do assert {:ok, %Station{} = st} = Weather.find_or_create_station(%{ station_code: "KAUS", station_type: "asos", name: "Austin", lat: 30.2, lon: -97.7 }) assert st.station_code == "KAUS" end test "returns existing station on duplicate" do {:ok, st1} = Weather.find_or_create_station(%{ station_code: "KORD", station_type: "asos", name: "Chicago", lat: 41.9, lon: -87.9 }) {:ok, st2} = Weather.find_or_create_station(%{ station_code: "KORD", station_type: "asos", name: "Chicago", lat: 41.9, lon: -87.9 }) assert st1.id == st2.id end test "string-keyed attrs work too" do assert {:ok, %Station{}} = Weather.find_or_create_station(%{ "station_code" => "KSEA", "station_type" => "asos", "name" => "Seattle", "lat" => 47.4, "lon" => -122.3 }) end end describe "has_surface_observations?/3" do test "returns false when no obs in window" do st = create_station(%{station_code: "KSAN"}) refute Weather.has_surface_observations?( st.id, ~U[2026-01-01 00:00:00Z], ~U[2026-01-02 00:00:00Z] ) end test "returns true when an obs exists in window" do st = create_station(%{station_code: "KSFO"}) {:ok, _} = Weather.upsert_surface_observation(st, %{ observed_at: ~U[2026-05-01 12:00:00Z], temp_f: 70.0 }) assert Weather.has_surface_observations?( st.id, ~U[2026-05-01 00:00:00Z], ~U[2026-05-02 00:00:00Z] ) end end describe "station_day_covered?/2" do test "false when no obs on that day" do st = create_station(%{station_code: "KMIA"}) refute Weather.station_day_covered?(st.id, ~D[2026-05-01]) end test "true after upserting an obs on that day" do st = create_station(%{station_code: "KBOS"}) {:ok, _} = Weather.upsert_surface_observation(st, %{ observed_at: ~U[2026-05-01 14:00:00Z], temp_f: 60.0 }) assert Weather.station_day_covered?(st.id, ~D[2026-05-01]) end end describe "station_day_pairs_covered/1 with rows" do test "returns the covered subset" do st = create_station(%{station_code: "KPHX"}) {:ok, _} = Weather.upsert_surface_observation(st, %{ observed_at: ~U[2026-05-01 06:00:00Z], temp_f: 80.0 }) result = Weather.station_day_pairs_covered([{st.id, ~D[2026-05-01]}, {st.id, ~D[2026-05-02]}]) assert MapSet.member?(result, {st.id, ~D[2026-05-01]}) end end describe "has_sounding?/2" do test "returns false when no sounding exists" do st = create_station(%{station_code: "KFWD", station_type: "sounding"}) refute Weather.has_sounding?(st.id, ~U[2026-05-01 12:00:00Z]) end end describe "station_ids_with_soundings/2" do test "empty result when none match" do st = create_station(%{station_code: "KOUN", station_type: "sounding"}) result = Weather.station_ids_with_soundings([st.id], [~U[2026-05-01 12:00:00Z]]) assert MapSet.size(result) == 0 end end describe "get_solar_index/1" do test "returns nil when none present" do assert Weather.get_solar_index(~D[2026-05-01]) == nil end test "returns the row after upsert" do {:ok, _} = Weather.upsert_solar_index(%{date: ~D[2026-05-03], sfi: 130.0}) idx = Weather.get_solar_index(~D[2026-05-03]) assert idx assert idx.sfi == 130.0 end end describe "existing_solar_dates/0" do test "empty MapSet when DB is empty" do assert Weather.existing_solar_dates() == MapSet.new() end test "contains dates after upserts" do {:ok, _} = Weather.upsert_solar_index(%{date: ~D[2026-05-04], sfi: 120.0}) {:ok, _} = Weather.upsert_solar_index(%{date: ~D[2026-05-05], sfi: 121.0}) dates = Weather.existing_solar_dates() assert MapSet.member?(dates, ~D[2026-05-04]) assert MapSet.member?(dates, ~D[2026-05-05]) end end describe "upsert_solar_indices_batch/1" do test "empty list returns 0" do assert Weather.upsert_solar_indices_batch([]) == 0 end test "inserts multiple rows" do records = [ %{date: ~D[2026-06-01], sfi: 100.0, ap_index: 5}, %{date: ~D[2026-06-02], sfi: 105.0, ap_index: 6} ] count = Weather.upsert_solar_indices_batch(records) assert count == 2 end end describe "nearby_stations/4" do test "returns empty when no stations in range" do assert Weather.nearby_stations(0.0, 0.0, "asos", 10.0) == [] end test "finds station within radius" do _ = create_station(%{station_code: "KDFW2", lat: 32.9, lon: -97.0}) results = Weather.nearby_stations(32.9, -97.0, "asos", 50.0) assert results != [] end end describe "sounding_times_around/1" do test "returns a list of DateTime values" do times = Weather.sounding_times_around(~U[2026-05-01 12:00:00Z]) assert is_list(times) assert Enum.all?(times, fn t -> match?(%DateTime{}, t) end) end end describe "latest_grid_valid_time/0" do test "returns nil when no rows" do assert Weather.latest_grid_valid_time() == nil end end describe "find_nearest_hrrr/3" do test "returns nil when no profile exists" do assert Weather.find_nearest_hrrr(32.9, -97.0, ~U[2026-05-01 12:00:00Z]) == nil end end describe "find_nearest_native_profile/3" do test "returns nil when no profile exists" do assert Weather.find_nearest_native_profile(32.9, -97.0, ~U[2026-05-01 12:00:00Z]) == nil end end describe "find_nearest_iemre/3" do test "returns nil when no observation exists" do assert Weather.find_nearest_iemre(32.9, -97.0, ~U[2026-05-01 12:00:00Z]) == nil end end describe "find_nearest_narr/3" do test "returns nil when no profile exists" do assert Weather.find_nearest_narr(32.9, -97.0, ~U[2026-05-01 12:00:00Z]) == nil end end describe "nearest_native_duct_ghz/3" do test "returns nil when no native profile" do assert Weather.nearest_native_duct_ghz(32.9, -97.0, ~U[2026-05-01 12:00:00Z]) == nil end end describe "nearest_native_duct_info/3" do test "returns an error tuple when no native profile" do assert match?({:error, _}, Weather.nearest_native_duct_info(32.9, -97.0, ~U[2026-05-01 12:00:00Z])) end end describe "nearest_sounding_to/4" do test "returns an error tuple when no soundings near point" do assert match?({:error, _}, Weather.nearest_sounding_to(32.9, -97.0, ~U[2026-05-01 12:00:00Z])) end end describe "best_profile_for_contact/1" do test "returns nil for contact missing pos1" do assert Weather.best_profile_for_contact(%{pos1: nil, qso_timestamp: ~U[2026-05-01 12:00:00Z]}) == nil end end describe "profiles_along_path/1" do test "returns empty list for contact missing pos1" do assert Weather.profiles_along_path(%{pos1: nil, pos2: %{"lat" => 33.0, "lon" => -97.0}}) == [] end end describe "upsert_iemre_observation/1" do test "inserts a new IEMRE observation" do attrs = %{ date: ~D[2026-05-01], lat: 32.9, lon: -97.0, hourly: [ %{"hour" => 0, "air_temp_f" => 65.0} ] } assert {:ok, _obs} = Weather.upsert_iemre_observation(attrs) end end describe "backfill_hrrr_scalars/1" do test "runs on empty DB without raising" do result = Weather.backfill_hrrr_scalars(batch_size: 5, max_batches: 1) assert is_integer(result) or match?({:ok, _}, result) or result == :ok end end describe "analyze_all/1" do test "runs on empty DB without raising" do _ = Weather.analyze_all(skip_recent_seconds: 0) assert true end end describe "reconcile_*_statuses (smoke)" do test "reconcile_iemre_statuses returns count tuple on empty DB" do assert {:ok, count} = Weather.reconcile_iemre_statuses() assert is_integer(count) end test "reconcile_hrrr_statuses returns count tuple on empty DB" do assert {:ok, count} = Weather.reconcile_hrrr_statuses() assert is_integer(count) end test "reconcile_weather_statuses returns count tuple on empty DB" do assert {:ok, count} = Weather.reconcile_weather_statuses() assert is_integer(count) end end end