defmodule Microwaveprop.IonosphereTest do use Microwaveprop.DataCase, async: true alias Microwaveprop.Ionosphere alias Microwaveprop.Ionosphere.Observation @valid_attrs %{ station_code: "MHJ45", valid_time: ~U[2026-04-15 18:00:00Z], confidence_score: 80, fo_f2_mhz: 7.113, fo_e_mhz: 3.10, fo_es_mhz: 3.08, hm_f2_km: 242.0, mufd_mhz: 23.00 } describe "upsert_observations/2" do test "inserts new observations and returns the inserted count" do assert {:ok, 2} = Ionosphere.upsert_observations("MHJ45", [ Map.delete(@valid_attrs, :station_code), @valid_attrs |> Map.delete(:station_code) |> Map.put(:valid_time, ~U[2026-04-15 18:07:30Z]) ]) assert Repo.aggregate(Observation, :count, :id) == 2 end test "re-upserting the same (station, valid_time) replaces fields and does not duplicate" do {:ok, 1} = Ionosphere.upsert_observations("MHJ45", [Map.delete(@valid_attrs, :station_code)]) updated = @valid_attrs |> Map.delete(:station_code) |> Map.put(:fo_es_mhz, 9.5) assert {:ok, 1} = Ionosphere.upsert_observations("MHJ45", [updated]) assert Repo.aggregate(Observation, :count, :id) == 1 [row] = Repo.all(Observation) assert row.fo_es_mhz == 9.5 end test "stamps station_code on each observation" do {:ok, 1} = Ionosphere.upsert_observations("AL945", [ Map.delete(@valid_attrs, :station_code) ]) [row] = Repo.all(Observation) assert row.station_code == "AL945" end test "returns 0 and does nothing for an empty list" do assert {:ok, 0} = Ionosphere.upsert_observations("MHJ45", []) assert Repo.aggregate(Observation, :count, :id) == 0 end end describe "latest_observation/1" do test "returns the most recent observation for a station" do older = Map.delete(@valid_attrs, :station_code) newer = @valid_attrs |> Map.delete(:station_code) |> Map.put(:valid_time, ~U[2026-04-15 19:00:00Z]) |> Map.put(:fo_es_mhz, 10.0) {:ok, _} = Ionosphere.upsert_observations("MHJ45", [older, newer]) row = Ionosphere.latest_observation("MHJ45") assert row.valid_time == ~U[2026-04-15 19:00:00Z] assert row.fo_es_mhz == 10.0 end test "returns nil when no observations exist for the station" do assert Ionosphere.latest_observation("MHJ45") == nil end test "filters by station_code (another station's newer row does not leak through)" do mhj_old = @valid_attrs |> Map.delete(:station_code) |> Map.put(:valid_time, ~U[2026-04-15 18:00:00Z]) |> Map.put(:fo_es_mhz, 4.0) al_newer = @valid_attrs |> Map.delete(:station_code) |> Map.put(:valid_time, ~U[2026-04-15 23:00:00Z]) |> Map.put(:fo_es_mhz, 11.0) {:ok, _} = Ionosphere.upsert_observations("MHJ45", [mhj_old]) {:ok, _} = Ionosphere.upsert_observations("AL945", [al_newer]) row = Ionosphere.latest_observation("MHJ45") assert row.station_code == "MHJ45" assert row.valid_time == ~U[2026-04-15 18:00:00Z] assert row.fo_es_mhz == 4.0 end end describe "nearest_foes/3" do setup do recent = DateTime.truncate(DateTime.utc_now(), :second) {:ok, _} = Ionosphere.upsert_observations("MHJ45", [ %{valid_time: recent, fo_es_mhz: 7.5, fo_f2_mhz: 8.0, mufd_mhz: 25.0} ]) {:ok, _} = Ionosphere.upsert_observations("AL945", [ %{valid_time: recent, fo_es_mhz: 12.3, fo_f2_mhz: 7.5, mufd_mhz: 24.0} ]) :ok end test "returns the nearest station's latest foEs for a grid point in New England" do # 42°N, -72°W is essentially on Millstone Hill (42.6, -71.5). assert {:ok, obs} = Ionosphere.nearest_foes(42.0, -72.0) assert obs.station_code == "MHJ45" assert obs.fo_es_mhz == 7.5 end test "returns the nearest station's latest foEs for a grid point in Michigan" do # 45°N, -84°W is next to Alpena (45.1, -83.6). assert {:ok, obs} = Ionosphere.nearest_foes(45.0, -84.0) assert obs.station_code == "AL945" assert obs.fo_es_mhz == 12.3 end test "returns {:error, :stale} when the nearest station's latest value is older than max_age" do stale = ~U[2020-01-01 00:00:00Z] Microwaveprop.Repo.update_all( Observation, set: [valid_time: stale] ) assert {:error, :stale} = Ionosphere.nearest_foes(42.0, -72.0, max_age_seconds: 3600) end test "returns {:error, :no_data} when no observations have been ingested" do Microwaveprop.Repo.delete_all(Observation) assert {:error, :no_data} = Ionosphere.nearest_foes(42.0, -72.0) end end end