defmodule Aprsme.GeoUtilsTest do use ExUnit.Case, async: true use ExUnitProperties alias Aprsme.GeoUtils doctest GeoUtils describe "haversine_distance/4" do test "calculates zero distance for same point" do assert GeoUtils.haversine_distance(33.16961, -96.4921, 33.16961, -96.4921) == 0.0 end test "calculates small distances accurately" do # About 1.1 meters north distance = GeoUtils.haversine_distance(33.16961, -96.4921, 33.16962, -96.4921) assert_in_delta distance, 1.11, 0.1 end test "calculates GPS drift-like distances" do # Typical GPS drift of ~5 meters distance = GeoUtils.haversine_distance(33.16961, -96.4921, 33.169655, -96.4921) assert_in_delta distance, 5.0, 0.5 end test "calculates larger distances" do # About 100 meters distance = GeoUtils.haversine_distance(33.16961, -96.4921, 33.17061, -96.4921) assert_in_delta distance, 111.2, 1.0 end test "handles nil inputs" do assert GeoUtils.haversine_distance(nil, -96.4921, 33.16962, -96.4921) == nil assert GeoUtils.haversine_distance(33.16961, nil, 33.16962, -96.4921) == nil assert GeoUtils.haversine_distance(33.16961, -96.4921, nil, -96.4921) == nil assert GeoUtils.haversine_distance(33.16961, -96.4921, 33.16962, nil) == nil end test "handles non-numeric inputs" do assert GeoUtils.haversine_distance("33.16961", -96.4921, 33.16962, -96.4921) == nil assert GeoUtils.haversine_distance(33.16961, "invalid", 33.16962, -96.4921) == nil end end describe "significant_movement?/5" do test "returns false for no movement" do refute GeoUtils.significant_movement?(33.16961, -96.4921, 33.16961, -96.4921) end test "returns false for GPS drift under default threshold" do # About 5 meters - typical GPS drift refute GeoUtils.significant_movement?(33.16961, -96.4921, 33.169655, -96.4921) end test "returns true for movement over default threshold" do # About 55 meters movement (over 50m default threshold) assert GeoUtils.significant_movement?(33.16961, -96.4921, 33.1701, -96.4921) end test "respects custom threshold" do # About 5 meters movement lat1 = 33.16961 lon1 = -96.4921 lat2 = 33.169655 lon2 = -96.4921 # With 3 meter threshold, should be significant assert GeoUtils.significant_movement?(lat1, lon1, lat2, lon2, 3) # With 10 meter threshold (default), should not be significant refute GeoUtils.significant_movement?(lat1, lon1, lat2, lon2, 10) # With 20 meter threshold, definitely not significant refute GeoUtils.significant_movement?(lat1, lon1, lat2, lon2, 20) end test "handles nil inputs" do refute GeoUtils.significant_movement?(nil, -96.4921, 33.16962, -96.4921) refute GeoUtils.significant_movement?(33.16961, nil, 33.16962, -96.4921) refute GeoUtils.significant_movement?(33.16961, -96.4921, nil, -96.4921) refute GeoUtils.significant_movement?(33.16961, -96.4921, 33.16962, nil) end end describe "property: haversine_distance" do property "is always non-negative for numeric inputs" do check all( lat1 <- float(min: -89.0, max: 89.0), lon1 <- float(min: -179.0, max: 179.0), lat2 <- float(min: -89.0, max: 89.0), lon2 <- float(min: -179.0, max: 179.0) ) do assert GeoUtils.haversine_distance(lat1, lon1, lat2, lon2) >= 0 end end property "is symmetric: d(a,b) == d(b,a)" do check all( lat1 <- float(min: -89.0, max: 89.0), lon1 <- float(min: -179.0, max: 179.0), lat2 <- float(min: -89.0, max: 89.0), lon2 <- float(min: -179.0, max: 179.0) ) do a = GeoUtils.haversine_distance(lat1, lon1, lat2, lon2) b = GeoUtils.haversine_distance(lat2, lon2, lat1, lon1) assert_in_delta a, b, 0.01 end end property "d(p, p) == 0" do check all( lat <- float(min: -89.0, max: 89.0), lon <- float(min: -179.0, max: 179.0) ) do assert GeoUtils.haversine_distance(lat, lon, lat, lon) == 0.0 end end end describe "property: significant_movement?" do property "agrees with the haversine + threshold check" do check all( lat1 <- float(min: -89.0, max: 89.0), lon1 <- float(min: -179.0, max: 179.0), lat2 <- float(min: -89.0, max: 89.0), lon2 <- float(min: -179.0, max: 179.0), threshold <- integer(1..1000) ) do d = GeoUtils.haversine_distance(lat1, lon1, lat2, lon2) assert GeoUtils.significant_movement?(lat1, lon1, lat2, lon2, threshold) == d > threshold end end end end