defmodule Microwaveprop.WeatherTest do use Microwaveprop.DataCase, async: true alias Microwaveprop.Weather @station_attrs %{ station_code: "KDFW", station_type: "asos", name: "Dallas/Fort Worth International", lat: 32.8998, lon: -97.0403, elevation_m: 171.0, state: "TX" } @sounding_station_attrs %{ station_code: "FWD", station_type: "sounding", wmo_number: 72_249, name: "Fort Worth", lat: 32.83, lon: -97.30 } describe "find_or_create_station/1" do test "creates a new station" do assert {:ok, station} = Weather.find_or_create_station(@station_attrs) assert station.station_code == "KDFW" assert station.station_type == "asos" end test "returns existing station if already created" do {:ok, first} = Weather.find_or_create_station(@station_attrs) {:ok, second} = Weather.find_or_create_station(@station_attrs) assert first.id == second.id end test "returns error for invalid attributes" do assert {:error, _changeset} = Weather.find_or_create_station(%{station_code: "X"}) end end describe "upsert_surface_observation/2" do test "inserts a new observation" do {:ok, station} = Weather.find_or_create_station(@station_attrs) obs_attrs = %{ observed_at: ~U[2026-03-28 18:00:00Z], temp_f: 75.0, dewpoint_f: 55.0, sky_condition: "SCT" } assert {:ok, obs} = Weather.upsert_surface_observation(station, obs_attrs) assert obs.temp_f == 75.0 assert obs.station_id == station.id end test "updates existing observation on conflict" do {:ok, station} = Weather.find_or_create_station(@station_attrs) obs_attrs = %{ observed_at: ~U[2026-03-28 18:00:00Z], temp_f: 75.0, sky_condition: "SCT" } {:ok, first} = Weather.upsert_surface_observation(station, obs_attrs) {:ok, second} = Weather.upsert_surface_observation(station, %{obs_attrs | temp_f: 78.0}) assert first.id == second.id assert second.temp_f == 78.0 end end describe "upsert_sounding/2" do @sample_profile [ %{"pres" => 1013.0, "hght" => 171, "tmpc" => 25.0, "dwpc" => 15.0, "drct" => 180, "sknt" => 10}, %{"pres" => 925.0, "hght" => 800, "tmpc" => 20.0, "dwpc" => 12.0, "drct" => 200, "sknt" => 15}, %{"pres" => 850.0, "hght" => 1500, "tmpc" => 15.0, "dwpc" => 8.0, "drct" => 220, "sknt" => 20} ] test "inserts a new sounding" do {:ok, station} = Weather.find_or_create_station(@sounding_station_attrs) sounding_attrs = %{ observed_at: ~U[2026-03-28 12:00:00Z], profile: @sample_profile, level_count: 3, surface_temp_c: 25.0 } assert {:ok, sounding} = Weather.upsert_sounding(station, sounding_attrs) assert sounding.level_count == 3 assert sounding.station_id == station.id end test "updates existing sounding on conflict" do {:ok, station} = Weather.find_or_create_station(@sounding_station_attrs) sounding_attrs = %{ observed_at: ~U[2026-03-28 12:00:00Z], profile: @sample_profile, level_count: 3, surface_temp_c: 25.0 } {:ok, first} = Weather.upsert_sounding(station, sounding_attrs) {:ok, second} = Weather.upsert_sounding(station, %{sounding_attrs | surface_temp_c: 26.0}) assert first.id == second.id assert second.surface_temp_c == 26.0 end end describe "weather_for_qso/2" do test "returns nearby surface observations within time window with station preloaded" do {:ok, station} = Weather.find_or_create_station(@station_attrs) {:ok, _obs} = Weather.upsert_surface_observation(station, %{ observed_at: ~U[2026-03-28 18:00:00Z], temp_f: 75.0 }) # QSO near the station in space and time result = Weather.weather_for_qso( %{lat: 32.90, lon: -97.05, timestamp: ~U[2026-03-28 18:30:00Z]}, radius_km: 100, time_window_hours: 3 ) assert length(result.surface_observations) == 1 obs = hd(result.surface_observations) assert obs.temp_f == 75.0 assert obs.station.station_code == "KDFW" end test "returns nearby soundings within time window with station preloaded" do {:ok, station} = Weather.find_or_create_station(@sounding_station_attrs) {:ok, _sounding} = Weather.upsert_sounding(station, %{ observed_at: ~U[2026-03-28 12:00:00Z], profile: [%{"pres" => 1013.0, "hght" => 171, "tmpc" => 25.0, "dwpc" => 15.0, "drct" => 180, "sknt" => 10}], level_count: 1, surface_temp_c: 25.0 }) result = Weather.weather_for_qso( %{lat: 32.85, lon: -97.28, timestamp: ~U[2026-03-28 14:00:00Z]}, radius_km: 100, time_window_hours: 6 ) assert length(result.soundings) == 1 assert hd(result.soundings).station.station_code == "FWD" end test "excludes observations outside radius" do {:ok, station} = Weather.find_or_create_station(@station_attrs) {:ok, _obs} = Weather.upsert_surface_observation(station, %{ observed_at: ~U[2026-03-28 18:00:00Z], temp_f: 75.0 }) # QSO far away result = Weather.weather_for_qso( %{lat: 40.0, lon: -80.0, timestamp: ~U[2026-03-28 18:00:00Z]}, radius_km: 100, time_window_hours: 3 ) assert result.surface_observations == [] end test "excludes observations outside time window" do {:ok, station} = Weather.find_or_create_station(@station_attrs) {:ok, _obs} = Weather.upsert_surface_observation(station, %{ observed_at: ~U[2026-03-28 06:00:00Z], temp_f: 75.0 }) # QSO much later result = Weather.weather_for_qso( %{lat: 32.90, lon: -97.05, timestamp: ~U[2026-03-28 22:00:00Z]}, radius_km: 100, time_window_hours: 3 ) assert result.surface_observations == [] end end describe "has_surface_observations?/3" do test "returns true when observations exist in the time window" do {:ok, station} = Weather.find_or_create_station(@station_attrs) Weather.upsert_surface_observation(station, %{ observed_at: ~U[2026-03-28 18:00:00Z], temp_f: 75.0 }) assert Weather.has_surface_observations?( station.id, ~U[2026-03-28 16:00:00Z], ~U[2026-03-28 20:00:00Z] ) end test "returns false when no observations exist in the time window" do {:ok, station} = Weather.find_or_create_station(@station_attrs) refute Weather.has_surface_observations?( station.id, ~U[2026-03-28 16:00:00Z], ~U[2026-03-28 20:00:00Z] ) end test "returns false when observations are outside the time window" do {:ok, station} = Weather.find_or_create_station(@station_attrs) Weather.upsert_surface_observation(station, %{ observed_at: ~U[2026-03-28 06:00:00Z], temp_f: 60.0 }) refute Weather.has_surface_observations?( station.id, ~U[2026-03-28 16:00:00Z], ~U[2026-03-28 20:00:00Z] ) end end describe "has_sounding?/2" do test "returns true when a sounding exists at the given time" do {:ok, station} = Weather.find_or_create_station(@sounding_station_attrs) Weather.upsert_sounding(station, %{ observed_at: ~U[2026-03-28 12:00:00Z], profile: [%{"pres" => 1013.0, "tmpc" => 25.0}], level_count: 1 }) assert Weather.has_sounding?(station.id, ~U[2026-03-28 12:00:00Z]) end test "returns false when no sounding exists at the given time" do {:ok, station} = Weather.find_or_create_station(@sounding_station_attrs) refute Weather.has_sounding?(station.id, ~U[2026-03-28 12:00:00Z]) end end describe "existing_solar_dates/0" do test "returns set of dates already in the database" do Weather.upsert_solar_index(%{date: ~D[2024-06-15], sfi: 150.2}) Weather.upsert_solar_index(%{date: ~D[2024-06-16], sfi: 145.0}) dates = Weather.existing_solar_dates() assert MapSet.member?(dates, ~D[2024-06-15]) assert MapSet.member?(dates, ~D[2024-06-16]) refute MapSet.member?(dates, ~D[2024-06-17]) end test "returns empty set when no solar indices exist" do assert Weather.existing_solar_dates() == MapSet.new() end end describe "get_solar_index/1" do test "returns the solar index record for a given date" do {:ok, record} = Weather.upsert_solar_index(%{date: ~D[2024-06-15], sfi: 150.2, sunspot_number: 120}) found = Weather.get_solar_index(~D[2024-06-15]) assert found.id == record.id assert found.sfi == 150.2 end test "returns nil when no record exists" do assert Weather.get_solar_index(~D[2024-01-01]) == nil end end describe "upsert_solar_index/1" do test "inserts a new solar index record" do attrs = %{ date: ~D[2024-06-15], sfi: 150.2, sfi_adjusted: 148.5, sunspot_number: 120, ap_index: 8, kp_values: [2.0, 1.667, 2.333, 3.0, 2.667, 1.333, 1.0, 1.667] } assert {:ok, record} = Weather.upsert_solar_index(attrs) assert record.date == ~D[2024-06-15] assert record.sfi == 150.2 assert record.sunspot_number == 120 assert record.ap_index == 8 assert length(record.kp_values) == 8 end test "updates existing record on date conflict" do attrs = %{ date: ~D[2024-06-15], sfi: 150.2, sfi_adjusted: 148.5, sunspot_number: 120, ap_index: 8, kp_values: [2.0, 1.667, 2.333, 3.0, 2.667, 1.333, 1.0, 1.667] } {:ok, first} = Weather.upsert_solar_index(attrs) {:ok, second} = Weather.upsert_solar_index(%{attrs | sfi: 155.0}) assert first.id == second.id assert second.sfi == 155.0 end test "inserts record with nil optional fields" do attrs = %{date: ~D[1932-01-01], sfi: nil, sfi_adjusted: nil, sunspot_number: 22, ap_index: 15} assert {:ok, record} = Weather.upsert_solar_index(attrs) assert record.date == ~D[1932-01-01] assert is_nil(record.sfi) assert record.sunspot_number == 22 end end end