defmodule Aprsme.DeviceParserTest do use ExUnit.Case, async: true use ExUnitProperties alias Aprsme.DeviceParser describe "extract_device_identifier/1" do test "returns the device_identifier atom-key value first" do assert DeviceParser.extract_device_identifier(%{device_identifier: "APRS123"}) == "APRS123" end test "returns the device_identifier string-key value" do assert DeviceParser.extract_device_identifier(%{"device_identifier" => "APRS123"}) == "APRS123" end test "falls back to destination when device_identifier missing" do assert DeviceParser.extract_device_identifier(%{destination: "APN382"}) == "APN382" assert DeviceParser.extract_device_identifier(%{"destination" => "APN382"}) == "APN382" end test "prefers device_identifier over destination" do packet = %{device_identifier: "DIRECT", destination: "APN382"} assert DeviceParser.extract_device_identifier(packet) == "DIRECT" end test "falls back to symbol table/code from atom-keyed data_extended" do packet = %{data_extended: %{symbol_table_id: "/", symbol_code: ">"}} assert DeviceParser.extract_device_identifier(packet) == "/>" end test "falls back to symbol table/code from string-keyed data_extended" do packet = %{"data_extended" => %{"symbol_table_id" => "\\", "symbol_code" => "k"}} assert DeviceParser.extract_device_identifier(packet) == "\\k" end test "prefers destination over data_extended symbol (destination clause matches first)" do # Pattern-match order in find_device_identifier/1 checks destination before data_extended. packet = %{ data_extended: %{symbol_table_id: "/", symbol_code: ">"}, destination: "APN382" } assert DeviceParser.extract_device_identifier(packet) == "APN382" end test "falls back to data_extended symbol when no destination is set" do packet = %{data_extended: %{symbol_table_id: "/", symbol_code: ">"}} assert DeviceParser.extract_device_identifier(packet) == "/>" end test "returns nil for empty device_identifier strings" do assert DeviceParser.extract_device_identifier(%{device_identifier: ""}) == nil end test "returns nil for empty destination strings" do assert DeviceParser.extract_device_identifier(%{destination: ""}) == nil end test "returns nil for empty map" do assert DeviceParser.extract_device_identifier(%{}) == nil end test "returns nil for non-map input" do assert DeviceParser.extract_device_identifier(nil) == nil assert DeviceParser.extract_device_identifier("hello") == nil assert DeviceParser.extract_device_identifier(42) == nil assert DeviceParser.extract_device_identifier([]) == nil end test "returns nil when device_identifier is a non-binary truthy value" do assert DeviceParser.extract_device_identifier(%{device_identifier: :atom}) == nil assert DeviceParser.extract_device_identifier(%{device_identifier: 123}) == nil end end describe "normalize_device_identifier/1" do test "trims whitespace from strings" do assert DeviceParser.normalize_device_identifier(" APRS123 ") == "APRS123" end test "leaves clean strings alone" do assert DeviceParser.normalize_device_identifier("APRS123") == "APRS123" end test "converts atoms to strings" do assert DeviceParser.normalize_device_identifier(:yaesu_ft1d) == "yaesu_ft1d" end test "returns nil for nil" do assert DeviceParser.normalize_device_identifier(nil) == nil end test "returns nil for unsupported types" do assert DeviceParser.normalize_device_identifier(123) == nil assert DeviceParser.normalize_device_identifier(%{}) == nil assert DeviceParser.normalize_device_identifier([]) == nil end property "strings round-trip through normalization without surrounding whitespace" do check all(s <- string(:alphanumeric, min_length: 1)) do assert DeviceParser.normalize_device_identifier(s) == s assert DeviceParser.normalize_device_identifier(" " <> s <> " ") == s end end property "atom inputs normalize to their string form" do check all(s <- string(:alphanumeric, min_length: 1, max_length: 10)) do atom = String.to_atom(s) assert DeviceParser.normalize_device_identifier(atom) == s end end end end