defmodule Microwaveprop.Radio.BandResolverPropertyTest do use ExUnit.Case, async: true use ExUnitProperties alias Microwaveprop.Radio.BandResolver describe "resolve/1 output contract" do property "result is always nil or a member of allowed_bands/0 (strings)" do check all(s <- string(:printable)) do case BandResolver.resolve(s) do nil -> :ok mhz -> assert mhz in BandResolver.allowed_bands() end end end property "result is always nil or a member of allowed_bands/0 (integers)" do check all(i <- integer(-1_000..500_000)) do case BandResolver.resolve(i) do nil -> :ok mhz -> assert mhz in BandResolver.allowed_bands() end end end property "result is always nil or a member of allowed_bands/0 (floats)" do check all(f <- float(min: -10.0, max: 500_000.0)) do case BandResolver.resolve(f) do nil -> :ok mhz -> assert mhz in BandResolver.allowed_bands() end end end property "never crashes on arbitrary term input" do check all( term <- one_of([ constant(nil), constant(""), constant(:atom), constant([]), constant(%{}), string(:printable), integer(), float() ]) ) do result = BandResolver.resolve(term) assert is_nil(result) or is_integer(result) end end end describe "resolve/1 is idempotent for canonical bands" do property "every allowed band resolves to itself" do check all(mhz <- member_of(BandResolver.allowed_bands())) do assert BandResolver.resolve(mhz) == mhz assert BandResolver.resolve(Integer.to_string(mhz)) == mhz end end property "resolve_as_string/1 round-trips through Integer.to_string" do check all(mhz <- member_of(BandResolver.allowed_bands())) do assert BandResolver.resolve_as_string(mhz) == Integer.to_string(mhz) end end end describe "resolve/1 snap-to-nearest" do property "frequencies within a small neighborhood of a microwave band resolve to that band" do # The >= 900 MHz branch uses nearest-neighbor snapping. A small # perturbation of a canonical band value must land on the same # band — but we clamp the perturbed input to stay ≥ 900 so we # don't cross the resolver's lower cutoff for 902 (the smallest # microwave band). bands_900_plus = Enum.filter(BandResolver.allowed_bands(), &(&1 >= 900)) check all( mhz <- member_of(bands_900_plus), delta_pct <- float(min: -0.001, max: 0.001) ) do perturbed = max(900.01, mhz * (1.0 + delta_pct)) # Only assert when the perturbed value is still unambiguously # closer to `mhz` than to any other allowed band. nearest_other = bands_900_plus |> Enum.reject(&(&1 == mhz)) |> Enum.min_by(&abs(&1 - perturbed)) if abs(perturbed - mhz) < abs(perturbed - nearest_other) do assert BandResolver.resolve(perturbed) == mhz end end end end end