defmodule Microwaveprop.Rover.ComputeTest do use ExUnit.Case, async: true alias Microwaveprop.Rover.Compute defp build_grid(home_lat, home_lon) do # 3x3 grid of cells around home spaced 0.1 deg apart for dlat <- [-0.1, 0.0, 0.1], dlon <- [-0.1, 0.0, 0.1] do %{ lat: Float.round(home_lat + dlat, 4), lon: Float.round(home_lon + dlon, 4), score: 80 } end end test "returns top_candidates ordered by score desc with required fields" do home = %{lat: 33.0, lon: -96.0, elev_m: 200} stations = [%{callsign: "W5LUA", lat: 33.10, lon: -96.625, selected: true}] grid = build_grid(home.lat, home.lon) scores_at = fn _band, _t, _bbox -> grid end elev_lookup = fn points -> Map.new(points, fn p -> {p, 250} end) end clearance_lookup = fn _cells, _stations -> %{} end prominence_lookup = fn cells -> Map.new(cells, fn c -> {{c.lat, c.lon}, 0} end) end road_lookup = fn _cells, _bbox -> {:error, :stubbed} end args = %{ home: home, stations: stations, band_mhz: 10_000, valid_time: ~U[2026-04-25 12:00:00Z], mode: :ssb, max_distance_km: 65.0, min_elev_gain: 0 } result = Compute.run(args, scores_at: scores_at, elev_lookup: elev_lookup, clearance_lookup: clearance_lookup, prominence_lookup: prominence_lookup, road_lookup: road_lookup ) assert is_map(result) assert Map.has_key?(result, :cells) assert Map.has_key?(result, :top_candidates) assert is_list(result.cells) assert length(result.top_candidates) <= 5 refute result.top_candidates == [] # Sorted by score desc. scores = Enum.map(result.top_candidates, & &1.score) assert scores == Enum.sort(scores, :desc) # Required candidate fields. cand = hd(result.top_candidates) for key <- [ :grid, :lat, :lon, :elev_m, :drive_min, :score, :tier_color, :distance_km, :bearing_compass, :name ] do assert Map.has_key?(cand, key), "expected candidate to have key #{inspect(key)}" end assert is_binary(cand.grid) assert is_binary(cand.tier_color) assert is_binary(cand.bearing_compass) assert is_binary(cand.name) end test "penalizes cells with tall nearby buildings (clutter)" do home = %{lat: 33.0, lon: -96.0, elev_m: 200} stations = [%{callsign: "W5LUA", lat: 33.10, lon: -96.625, selected: true}] grid = build_grid(home.lat, home.lon) tall_cell = {Float.round(home.lat + 0.1, 4), Float.round(home.lon, 4)} ref_cell = {Float.round(home.lat - 0.1, 4), Float.round(home.lon, 4)} scores_at = fn _band, _t, _bbox -> grid end elev_lookup = fn points -> Map.new(points, fn p -> {p, 250} end) end clearance_lookup = fn _cells, _stations -> %{} end prominence_lookup = fn cells -> Map.new(cells, fn c -> {{c.lat, c.lon}, 0} end) end road_lookup = fn _cells, _bbox -> {:error, :stubbed} end buildings_clutter_lookup = fn cells -> Map.new(cells, fn c -> height = if {c.lat, c.lon} == tall_cell, do: 30.0, else: 0.0 {{c.lat, c.lon}, height} end) end args = %{ home: home, stations: stations, band_mhz: 10_000, valid_time: ~U[2026-04-25 12:00:00Z], mode: :ssb, max_distance_km: 65.0, min_elev_gain: 0 } result = Compute.run(args, scores_at: scores_at, elev_lookup: elev_lookup, clearance_lookup: clearance_lookup, prominence_lookup: prominence_lookup, road_lookup: road_lookup, buildings_clutter_lookup: buildings_clutter_lookup ) cells_by_pos = Map.new(result.cells, fn c -> {{c.lat, c.lon}, c.score} end) tall_score = Map.fetch!(cells_by_pos, tall_cell) ref_score = Map.fetch!(cells_by_pos, ref_cell) # 30m nearby building → penalty = 30/5 = 6.0 dB assert ref_score - tall_score >= 5.5 end test "calls progress callback with each pipeline step label" do home = %{lat: 33.0, lon: -96.0, elev_m: 200} stations = [%{callsign: "W5LUA", lat: 33.10, lon: -96.625, selected: true}] grid = build_grid(home.lat, home.lon) test_pid = self() progress = fn label, step, total -> send(test_pid, {:progress, label, step, total}) end Compute.run( %{ home: home, stations: stations, band_mhz: 10_000, valid_time: ~U[2026-04-25 12:00:00Z], mode: :ssb, max_distance_km: 65.0, min_elev_gain: 0 }, scores_at: fn _, _, _ -> grid end, elev_lookup: fn points -> Map.new(points, fn p -> {p, 250} end) end, clearance_lookup: fn _, _ -> %{} end, prominence_lookup: fn cells -> Map.new(cells, fn c -> {{c.lat, c.lon}, 0} end) end, road_lookup: fn _, _ -> {:error, :stubbed} end, progress: progress ) assert_received {:progress, "Loading propagation grid", 1, total} assert is_integer(total) and total >= 7 assert_received {:progress, "Looking up elevation", 2, ^total} assert_received {:progress, "Computing path clearance", _, ^total} assert_received {:progress, "Scoring cells", ^total, ^total} end test "filters cells failing min_elev_gain" do home = %{lat: 33.0, lon: -96.0, elev_m: 500} stations = [%{callsign: "W5LUA", lat: 33.1, lon: -96.6, selected: true}] grid = build_grid(home.lat, home.lon) # All elevations below home — nothing should pass min_elev_gain=200. scores_at = fn _band, _t, _bbox -> grid end elev_lookup = fn points -> Map.new(points, fn p -> {p, 100} end) end clearance_lookup = fn _cells, _stations -> %{} end prominence_lookup = fn cells -> Map.new(cells, fn c -> {{c.lat, c.lon}, 0} end) end road_lookup = fn _cells, _bbox -> {:error, :stubbed} end result = Compute.run( %{ home: home, stations: stations, band_mhz: 10_000, valid_time: ~U[2026-04-25 12:00:00Z], mode: :ssb, max_distance_km: 65.0, min_elev_gain: 200 }, scores_at: scores_at, elev_lookup: elev_lookup, clearance_lookup: clearance_lookup, prominence_lookup: prominence_lookup, road_lookup: road_lookup ) assert result.cells == [] assert result.top_candidates == [] end end