defmodule Microwaveprop.Beacons.RangeEstimateTest do use Microwaveprop.DataCase alias Microwaveprop.Beacons.Beacon alias Microwaveprop.Beacons.RangeEstimate defp beacon(overrides \\ %{}) do base = %Beacon{ id: "00000000-0000-0000-0000-000000000000", frequency_mhz: 10_368.1, callsign: "W5HN", grid: "EM12", lat: 32.897, lon: -97.038, power_mw: 10_000.0, height_ft: 100, on_the_air: true } struct!(base, overrides) end describe "mw_to_dbm/1" do test "1 mW is 0 dBm" do assert_in_delta RangeEstimate.mw_to_dbm(1.0), 0.0, 0.001 end test "1 W (1000 mW) is 30 dBm" do assert_in_delta RangeEstimate.mw_to_dbm(1000.0), 30.0, 0.001 end test "10 W (10_000 mW) is 40 dBm" do assert_in_delta RangeEstimate.mw_to_dbm(10_000.0), 40.0, 0.001 end end describe "nearest_band_mhz/1" do test "10368.1 MHz maps to 10000 (10 GHz band)" do assert RangeEstimate.nearest_band_mhz(10_368.1) == 10_000 end test "24192.1 MHz maps to 24000" do assert RangeEstimate.nearest_band_mhz(24_192.1) == 24_000 end test "47088 MHz maps to 47000" do assert RangeEstimate.nearest_band_mhz(47_088.0) == 47_000 end end describe "estimate/1" do test "returns a map with band info, center score, and per-grid cells" do result = RangeEstimate.estimate(beacon()) assert result.band_mhz == 10_000 assert is_integer(result.center_score) or is_float(result.center_score) assert is_float(result.eirp_dbm) assert is_list(result.cells) assert result.cells != [] assert result.grid_step == 0.125 assert is_list(result.tiers) end test "each cell carries lat/lon, distance, rx_dbm, tier label/color" do result = RangeEstimate.estimate(beacon()) for cell <- result.cells do assert is_float(cell.lat) assert is_float(cell.lon) assert is_float(cell.distance_km) assert is_integer(cell.rx_dbm) assert is_binary(cell.label) assert is_binary(cell.color) # Detection floor enforced assert cell.rx_dbm >= -145 end end test "cells closer to the beacon have stronger rx_dbm" do result = RangeEstimate.estimate(beacon()) [closest | _] = Enum.sort_by(result.cells, & &1.distance_km) [farthest | _] = Enum.sort_by(result.cells, &(-&1.distance_km)) assert closest.rx_dbm > farthest.rx_dbm end test "a more powerful beacon produces more cells" do weak = RangeEstimate.estimate(beacon(power_mw: 10.0)) strong = RangeEstimate.estimate(beacon(power_mw: 10_000.0)) assert length(strong.cells) > length(weak.cells) end test "uses a default score of 50 when no propagation data exists" do result = RangeEstimate.estimate(beacon()) # No scores in test DB → center defaults, cells all use 50. assert result.center_score == 50 assert result.valid_time == nil # All cells should have score 50 when there's no HRRR data. assert Enum.all?(result.cells, fn c -> c.score == 50 end) end test "cells align with the 0.125° HRRR grid" do result = RangeEstimate.estimate(beacon()) for cell <- result.cells do # lat and lon should both be multiples of 0.125 lat_mod = abs(cell.lat / 0.125 - round(cell.lat / 0.125)) lon_mod = abs(cell.lon / 0.125 - round(cell.lon / 0.125)) assert lat_mod < 0.01 assert lon_mod < 0.01 end end test "short-circuits with empty cells for beacons below 5.76 GHz" do # 432 MHz with 1 W at unrestricted range would generate >100k cells # because atmospheric loss is negligible — encoding that payload # into the map's data attribute was locking up browsers in prod. result = RangeEstimate.estimate(beacon(frequency_mhz: 432.38, power_mw: 1000.0)) assert result.cells == [] assert result.max_range_km == 0.0 # Still reports the nearest configured band for display purposes. assert is_integer(result.band_mhz) end test "short-circuits at exactly 5760 MHz threshold" do above = RangeEstimate.estimate(beacon(frequency_mhz: 5760.0, power_mw: 1000.0)) below = RangeEstimate.estimate(beacon(frequency_mhz: 5759.9, power_mw: 1000.0)) assert above.cells != [] assert below.cells == [] end end end