defmodule AprsmeWeb.Live.Shared.PacketUtilsTest do
  use ExUnit.Case, async: true

  alias AprsmeWeb.Live.Shared.PacketUtils

  describe "get_callsign_key/1" do
    test "returns sender callsign when no object/item name" do
      assert PacketUtils.get_callsign_key(%{sender: "K5ABC"}) == "K5ABC"
    end

    test "prefers object_name when present" do
      packet = %{sender: "K5ABC", object_name: "REPEATER1", data_type: "object"}
      assert PacketUtils.get_callsign_key(packet) == "REPEATER1"
    end

    test "prefers item_name when present" do
      packet = %{sender: "K5ABC", item_name: "EVENT", data_type: "item"}
      assert PacketUtils.get_callsign_key(packet) == "EVENT"
    end

    test "object_name wins over item_name" do
      packet = %{object_name: "OBJ", item_name: "ITEM", sender: "K5ABC"}
      assert PacketUtils.get_callsign_key(packet) == "OBJ"
    end

    test "returns __unknown__ for non-maps" do
      assert PacketUtils.get_callsign_key(nil) == "__unknown__"
    end

    test "returns __unknown__ when every field is empty" do
      packet = %{sender: "", object_name: "", item_name: ""}
      assert PacketUtils.get_callsign_key(packet) == "__unknown__"
    end

    test "strips whitespace from object/item names" do
      assert PacketUtils.get_callsign_key(%{object_name: "  REPEATER  "}) == "REPEATER"
    end
  end

  describe "prune_oldest_packets/2" do
    test "returns the same map when size is within limit" do
      packets = %{"a" => %{}, "b" => %{}}
      assert PacketUtils.prune_oldest_packets(packets, 10) == packets
    end

    test "keeps the newest packets by received_at DateTime" do
      now = DateTime.utc_now()
      older = DateTime.add(now, -3600, :second)
      oldest = DateTime.add(now, -7200, :second)

      packets = %{
        "a" => %{received_at: now},
        "b" => %{received_at: older},
        "c" => %{received_at: oldest}
      }

      result = PacketUtils.prune_oldest_packets(packets, 2)
      assert map_size(result) == 2
      assert Map.has_key?(result, "a")
      assert Map.has_key?(result, "b")
      refute Map.has_key?(result, "c")
    end

    test "supports integer timestamps via string-keyed received_at" do
      packets = %{
        "a" => %{"received_at" => 1000},
        "b" => %{"received_at" => 500}
      }

      result = PacketUtils.prune_oldest_packets(packets, 1)
      assert Map.keys(result) == ["a"]
    end

    test "treats packets without received_at as oldest (timestamp 0)" do
      now = DateTime.utc_now()
      packets = %{"a" => %{received_at: now}, "b" => %{no_date: true}}
      result = PacketUtils.prune_oldest_packets(packets, 1)
      assert Map.has_key?(result, "a")
    end

    test "prunes to zero when max_count is 0 and size exceeds it" do
      packets = %{"a" => %{received_at: DateTime.utc_now()}}
      assert PacketUtils.prune_oldest_packets(packets, 0) == %{}
    end
  end

  describe "parse_trail_duration/1" do
    test "accepts known values" do
      for hours <- [1, 6, 12, 24, 48, 168] do
        assert PacketUtils.parse_trail_duration(to_string(hours)) == hours
      end
    end

    test "rejects arbitrary numbers not in allowlist" do
      assert PacketUtils.parse_trail_duration("3") == 1
      assert PacketUtils.parse_trail_duration("100") == 1
      assert PacketUtils.parse_trail_duration("-1") == 1
    end

    test "returns default for garbage and non-string input" do
      assert PacketUtils.parse_trail_duration("abc") == 1
      assert PacketUtils.parse_trail_duration("24abc") == 1
      assert PacketUtils.parse_trail_duration(nil) == 1
      assert PacketUtils.parse_trail_duration(24) == 1
    end
  end

  describe "parse_historical_hours/1" do
    test "accepts known values" do
      for hours <- [1, 3, 6, 12, 24] do
        assert PacketUtils.parse_historical_hours(to_string(hours)) == hours
      end
    end

    test "rejects values not in allowlist" do
      assert PacketUtils.parse_historical_hours("48") == 1
      assert PacketUtils.parse_historical_hours("0") == 1
    end

    test "returns default for garbage" do
      assert PacketUtils.parse_historical_hours(nil) == 1
      assert PacketUtils.parse_historical_hours("abc") == 1
    end
  end

  describe "get_packet_field/3" do
    test "retrieves atom key" do
      assert PacketUtils.get_packet_field(%{sender: "K5ABC"}, :sender) == "K5ABC"
    end

    test "falls back to string key" do
      assert PacketUtils.get_packet_field(%{"sender" => "K5ABC"}, :sender) == "K5ABC"
    end

    test "falls back to data_extended atom key" do
      assert PacketUtils.get_packet_field(%{data_extended: %{temperature: 72}}, :temperature) == 72
    end

    test "falls back to data_extended string key" do
      assert PacketUtils.get_packet_field(%{"data_extended" => %{"humidity" => 50}}, :humidity) == 50
    end

    test "returns default when no match" do
      assert PacketUtils.get_packet_field(%{}, :whatever, :fallback) == :fallback
    end

    test "defaults to nil when no default given" do
      assert PacketUtils.get_packet_field(%{}, :whatever) == nil
    end

    test "handles nil data_extended gracefully" do
      assert PacketUtils.get_packet_field(%{data_extended: nil}, :temperature, 0) == 0
    end
  end

  describe "get_weather_field/2" do
    test "reads from top-level atom key" do
      assert PacketUtils.get_weather_field(%{temperature: 72}, :temperature) == 72
    end

    test "reads from data_extended" do
      packet = %{data_extended: %{pressure: 1013}}
      assert PacketUtils.get_weather_field(packet, :pressure) == 1013
    end

    test "returns nil when absent" do
      assert PacketUtils.get_weather_field(%{}, :temperature) == nil
    end
  end

  describe "has_weather_data?/1" do
    test "detects weather data in top-level fields" do
      assert PacketUtils.has_weather_data?(%{temperature: 72})
      assert PacketUtils.has_weather_data?(%{humidity: 50})
    end

    test "detects weather data in data_extended" do
      assert PacketUtils.has_weather_data?(%{data_extended: %{temperature: 72}})
    end

    test "returns false when no weather fields are present" do
      refute PacketUtils.has_weather_data?(%{sender: "K5ABC"})
      refute PacketUtils.has_weather_data?(%{})
    end
  end

  describe "packet_within_time_threshold?/2" do
    test "returns true when packet is after threshold" do
      now = DateTime.utc_now()
      older = DateTime.add(now, -3600, :second)
      assert PacketUtils.packet_within_time_threshold?(%{received_at: now}, older)
    end

    test "returns true when packet equals threshold" do
      now = DateTime.utc_now()
      assert PacketUtils.packet_within_time_threshold?(%{received_at: now}, now)
    end

    test "returns false when packet is older than threshold" do
      now = DateTime.utc_now()
      older = DateTime.add(now, -3600, :second)
      refute PacketUtils.packet_within_time_threshold?(%{received_at: older}, now)
    end

    test "treats packet with no received_at as within threshold" do
      assert PacketUtils.packet_within_time_threshold?(%{}, DateTime.utc_now())
    end

    test "accepts integer epoch threshold" do
      now = DateTime.utc_now()
      older_epoch = DateTime.to_unix(now) - 3600
      assert PacketUtils.packet_within_time_threshold?(%{received_at: now}, older_epoch)
    end

    test "accepts ISO8601 string threshold" do
      now = DateTime.utc_now()
      older = now |> DateTime.add(-3600, :second) |> DateTime.to_iso8601()
      assert PacketUtils.packet_within_time_threshold?(%{received_at: now}, older)
    end
  end

  describe "get_timestamp/1" do
    test "returns ISO8601 for DateTime" do
      dt = ~U[2024-01-15 12:00:00Z]
      assert PacketUtils.get_timestamp(%{received_at: dt}) == "2024-01-15T12:00:00Z"
    end

    test "returns ISO8601 for NaiveDateTime" do
      ndt = ~N[2024-01-15 12:00:00]
      result = PacketUtils.get_timestamp(%{received_at: ndt})
      assert result =~ "2024-01-15T12:00:00"
    end

    test "reformats ISO8601 strings" do
      # Any valid ISO8601 string parses and re-emits.
      assert PacketUtils.get_timestamp(%{received_at: "2024-01-15T12:00:00Z"}) ==
               "2024-01-15T12:00:00Z"
    end

    test "passes through unparseable strings" do
      assert PacketUtils.get_timestamp(%{received_at: "whenever"}) == "whenever"
    end

    test "returns empty string for nil or missing" do
      assert PacketUtils.get_timestamp(%{received_at: nil}) == ""
      assert PacketUtils.get_timestamp(%{}) == ""
    end
  end

  describe "generate_callsign/1" do
    test "joins base_callsign and ssid with a dash" do
      assert PacketUtils.generate_callsign(%{base_callsign: "K5ABC", ssid: "9"}) == "K5ABC-9"
    end

    test "returns base_callsign only when ssid is nil" do
      assert PacketUtils.generate_callsign(%{base_callsign: "K5ABC", ssid: nil}) == "K5ABC"
    end

    test "returns base_callsign only when ssid is empty" do
      assert PacketUtils.generate_callsign(%{base_callsign: "K5ABC", ssid: ""}) == "K5ABC"
    end

    test "returns empty when base_callsign is missing" do
      assert PacketUtils.generate_callsign(%{}) == ""
    end
  end

  describe "display_name/1" do
    test "prefers object_name over everything" do
      packet = %{sender: "K5ABC", object_name: "REP", item_name: "ITEM"}
      assert PacketUtils.display_name(packet) == "REP"
    end

    test "falls back to item_name when no object_name" do
      packet = %{sender: "K5ABC", item_name: "ITEM"}
      assert PacketUtils.display_name(packet) == "ITEM"
    end

    test "falls back to sender when neither is present" do
      assert PacketUtils.display_name(%{sender: "K5ABC"}) == "K5ABC"
    end

    test "strips whitespace from names" do
      assert PacketUtils.display_name(%{object_name: "  REP  "}) == "REP"
    end

    test "ignores blank object_name/item_name (treated as absent)" do
      packet = %{sender: "K5ABC", object_name: "   ", item_name: ""}
      assert PacketUtils.display_name(packet) == "K5ABC"
    end
  end

  describe "map_label/1" do
    test "returns object_name for data_type=object" do
      packet = %{data_type: "object", object_name: "REP", sender: "K5ABC"}
      assert PacketUtils.map_label(packet) == "REP"
    end

    test "returns item_name for data_type=item" do
      packet = %{data_type: "item", item_name: "EVENT", sender: "K5ABC"}
      assert PacketUtils.map_label(packet) == "EVENT"
    end

    test "falls back to sender when object_name empty on object type" do
      packet = %{data_type: "object", object_name: "", sender: "K5ABC"}
      assert PacketUtils.map_label(packet) == "K5ABC"
    end

    test "returns sender for position packets" do
      packet = %{data_type: "position", sender: "K5ABC"}
      assert PacketUtils.map_label(packet) == "K5ABC"
    end
  end

  describe "filter_packets_by_time_and_bounds/3" do
    test "filters by both predicates" do
      now = DateTime.utc_now()
      older = DateTime.add(now, -3600, :second)
      threshold = DateTime.add(now, -1800, :second)
      bounds = %{north: 50.0, south: 30.0, east: -90.0, west: -110.0}

      packets = %{
        "in_and_recent" => %{lat: 40.0, lon: -100.0, received_at: now},
        "in_but_old" => %{lat: 40.0, lon: -100.0, received_at: older},
        "out_and_recent" => %{lat: 0.0, lon: 0.0, received_at: now}
      }

      result = PacketUtils.filter_packets_by_time_and_bounds(packets, bounds, threshold)
      assert Map.keys(result) == ["in_and_recent"]
    end
  end
end
