defmodule Microwaveprop.Radio.MaidenheadTest do use ExUnit.Case, async: true alias Microwaveprop.Radio.Maidenhead describe "valid?/1" do test "accepts valid 4-char grid" do assert Maidenhead.valid?("EM12") end test "accepts valid 6-char grid" do assert Maidenhead.valid?("EM12ab") end test "is case-insensitive" do assert Maidenhead.valid?("em12") assert Maidenhead.valid?("EM12AB") assert Maidenhead.valid?("em12ab") end test "rejects field chars outside A-R" do refute Maidenhead.valid?("SZ12") end test "rejects subsquare chars outside a-x" do refute Maidenhead.valid?("EM12yz") end test "rejects wrong length" do refute Maidenhead.valid?("EM1") refute Maidenhead.valid?("EM123") refute Maidenhead.valid?("EM12abc") end test "rejects non-digit in square positions" do refute Maidenhead.valid?("EMAB") end test "rejects empty string" do refute Maidenhead.valid?("") end test "rejects nil" do refute Maidenhead.valid?(nil) end end describe "to_latlon/1" do test "returns center of 4-char grid FN31" do # FN31: F=5, N=13 -> lon = 5*20 - 180 + 3*2 + 1 = -73, lat = 13*10 - 90 + 1*1 + 0.5 = 41.5 assert {:ok, {lat, lon}} = Maidenhead.to_latlon("FN31") assert_in_delta lat, 41.5, 0.01 assert_in_delta lon, -73.0, 0.01 end test "returns center of 6-char grid FN31pr" do # FN31pr: field F=5,N=13; square 3,1; subsquare p=15,r=17 # lon = 5*20 - 180 + 3*2 + 15*(5/60) + (5/120) = -100 + 6 + 1.25 + 0.0417 = -72.7083 # lat = 13*10 - 90 + 1*1 + 17*(2.5/60) + (2.5/120) = 40 + 1 + 0.7083 + 0.02083 = 41.7292 assert {:ok, {lat, lon}} = Maidenhead.to_latlon("FN31pr") assert_in_delta lat, 41.7292, 0.01 assert_in_delta lon, -72.7083, 0.01 end test "is case-insensitive" do assert {:ok, {lat1, lon1}} = Maidenhead.to_latlon("FN31") assert {:ok, {lat2, lon2}} = Maidenhead.to_latlon("fn31") assert lat1 == lat2 assert lon1 == lon2 end test "returns :error for invalid grid" do assert :error = Maidenhead.to_latlon("ZZ99") end test "returns :error for nil" do assert :error = Maidenhead.to_latlon(nil) end test "returns :error for non-binary" do assert :error = Maidenhead.to_latlon(123) end test "handles 8-char extended grid EM12kp37" do assert {:ok, {lat, lon}} = Maidenhead.to_latlon("EM12kp37") assert is_float(lat) assert is_float(lon) # Should be within EM12kp subsquare assert lat > 32.0 and lat < 33.0 assert lon > -98.0 and lon < -96.0 end test "AA00 returns southwest corner center" do # lon = 0*20 - 180 + 0*2 + 1 = -179, lat = 0*10 - 90 + 0*1 + 0.5 = -89.5 assert {:ok, {lat, lon}} = Maidenhead.to_latlon("AA00") assert_in_delta lat, -89.5, 0.01 assert_in_delta lon, -179.0, 0.01 end test "RR99 returns northeast corner center" do # lon = 17*20 - 180 + 9*2 + 1 = 179, lat = 17*10 - 90 + 9*1 + 0.5 = 89.5 assert {:ok, {lat, lon}} = Maidenhead.to_latlon("RR99") assert_in_delta lat, 89.5, 0.01 assert_in_delta lon, 179.0, 0.01 end end describe "from_latlon/3" do test "round-trips 4-char grid FN31" do {:ok, {lat, lon}} = Maidenhead.to_latlon("FN31") assert Maidenhead.from_latlon(lat, lon, 4) == "FN31" end test "round-trips 6-char grid FN31pr" do {:ok, {lat, lon}} = Maidenhead.to_latlon("FN31pr") assert Maidenhead.from_latlon(lat, lon, 6) == "FN31pr" end test "defaults to precision 6" do {:ok, {lat, lon}} = Maidenhead.to_latlon("EM12kp") assert Maidenhead.from_latlon(lat, lon) == "EM12kp" end test "round-trips 8-char grid EM12kp37" do {:ok, {lat, lon}} = Maidenhead.to_latlon("EM12kp37") assert Maidenhead.from_latlon(lat, lon, 8) == "EM12kp37" end test "handles North Texas coordinates" do # DFW area is approximately EM12/EM13 grid = Maidenhead.from_latlon(32.897, -97.038, 6) assert String.starts_with?(grid, "EM12") end test "clamps the upper field boundary at 90 latitude / 180 longitude" do # Without clamping, lat = 90 + 90 = 180 → trunc(180/10) = 18, which # the field encoder turns into 'S' — outside the legal A-R range. # The same overflow happens at lon = 180. Both must round into the # last valid field instead of producing an invalid grid character. grid = Maidenhead.from_latlon(90.0, 180.0, 4) assert Maidenhead.valid?(grid) <> = grid assert f1 in ?A..?R assert f2 in ?A..?R end end end