defmodule Microwaveprop.Aprs.PathParserTest do
  use ExUnit.Case, async: true

  alias Microwaveprop.Aprs.PathParser

  @sender "N5XXX-9"
  @sender_pos {32.897, -97.038}
  @heard_at ~U[2026-04-30 12:00:00Z]

  defp lookup_fn(map) do
    fn callsign -> Map.get(map, callsign) end
  end

  describe "parse_hops/5" do
    test "single used-flag callsign emits one hop from sender" do
      lookup = lookup_fn(%{"K5GVL-10" => {32.95, -96.10}})

      hops =
        PathParser.parse_hops(
          @sender,
          @sender_pos,
          "K5GVL-10*,WIDE1-1,WIDE2-1",
          @heard_at,
          lookup
        )

      assert [hop] = hops
      assert hop.src_callsign == @sender
      assert hop.src_pos == @sender_pos
      assert hop.dst_callsign == "K5GVL-10"
      assert hop.dst_pos == {32.95, -96.10}
      assert hop.heard_at == @heard_at
      assert is_float(hop.distance_km)
    end

    test "two chained used-flag callsigns emit two hops, source advances" do
      lookup =
        lookup_fn(%{
          "K5XYZ-3" => {33.0, -96.5},
          "W5ABC-7" => {33.5, -95.5}
        })

      hops =
        PathParser.parse_hops(
          @sender,
          @sender_pos,
          "K5XYZ-3*,W5ABC-7*,WIDE2-1",
          @heard_at,
          lookup
        )

      assert length(hops) == 2
      [first, second] = hops

      assert first.src_callsign == @sender
      assert first.src_pos == @sender_pos
      assert first.dst_callsign == "K5XYZ-3"
      assert first.dst_pos == {33.0, -96.5}

      assert second.src_callsign == "K5XYZ-3"
      assert second.src_pos == {33.0, -96.5}
      assert second.dst_callsign == "W5ABC-7"
      assert second.dst_pos == {33.5, -95.5}
    end

    test "anonymous WIDE1* used-flag yields no hops" do
      lookup = lookup_fn(%{})

      hops =
        PathParser.parse_hops(
          @sender,
          @sender_pos,
          "WIDE1*,WIDE2-1",
          @heard_at,
          lookup
        )

      assert hops == []
    end

    test "mixed path with no used real callsigns yields no hops" do
      lookup = lookup_fn(%{"K5VOM-10" => {32.5, -96.5}})

      hops =
        PathParser.parse_hops(
          @sender,
          @sender_pos,
          "WA5VHU-8,WIDE1*,WIDE2-1,qAR,K5VOM-10",
          @heard_at,
          lookup
        )

      assert hops == []
    end

    test "trailing unused real callsign does not emit a hop" do
      lookup =
        lookup_fn(%{
          "K5GVL-10" => {32.95, -96.10},
          "N5TXZ-10" => {33.1, -96.2}
        })

      hops =
        PathParser.parse_hops(
          @sender,
          @sender_pos,
          "K5GVL-10*,N5TXZ-10",
          @heard_at,
          lookup
        )

      assert [hop] = hops
      assert hop.dst_callsign == "K5GVL-10"
    end

    test "TCPIP and Q-construct yield no hops" do
      lookup = lookup_fn(%{})

      hops =
        PathParser.parse_hops(
          @sender,
          @sender_pos,
          "TCPIP*,qAC,T2TEXAS",
          @heard_at,
          lookup
        )

      assert hops == []
    end

    test "lookup returns nil for a used digi → drop that hop and do NOT advance source" do
      lookup =
        lookup_fn(%{
          "K5XYZ-3" => {33.0, -96.5},
          # W5ABC-7 deliberately missing
          "K5DEF-1" => {33.5, -95.5}
        })

      hops =
        PathParser.parse_hops(
          @sender,
          @sender_pos,
          "K5XYZ-3*,W5ABC-7*,K5DEF-1*",
          @heard_at,
          lookup
        )

      assert length(hops) == 2
      # First hop is sender → K5XYZ-3 as before.
      assert Enum.at(hops, 0).src_callsign == @sender
      assert Enum.at(hops, 0).dst_callsign == "K5XYZ-3"
      # Critical: second emitted hop chains from K5XYZ-3 (NOT from W5ABC-7
      # which was dropped). Proves source did not advance through the
      # missing-position digi.
      assert Enum.at(hops, 1).src_callsign == "K5XYZ-3"
      assert Enum.at(hops, 1).dst_callsign == "K5DEF-1"
    end

    test "empty path string yields no hops" do
      lookup = lookup_fn(%{})

      hops = PathParser.parse_hops(@sender, @sender_pos, "", @heard_at, lookup)

      assert hops == []
    end

    test "aliases-only path yields no hops" do
      lookup = lookup_fn(%{})

      hops =
        PathParser.parse_hops(
          @sender,
          @sender_pos,
          "WIDE1-1,WIDE2-2",
          @heard_at,
          lookup
        )

      assert hops == []
    end

    test "lowercase Q-construct qAo is recognized and yields no hops" do
      lookup = lookup_fn(%{})

      hops =
        PathParser.parse_hops(
          @sender,
          @sender_pos,
          "qAo",
          @heard_at,
          lookup
        )

      assert hops == []
    end

    test "Q-construct followed by IGate name yields no hops" do
      lookup = lookup_fn(%{"IGATE-CALL" => {33.0, -96.0}})

      hops =
        PathParser.parse_hops(
          @sender,
          @sender_pos,
          "qAR,IGATE-CALL",
          @heard_at,
          lookup
        )

      assert hops == []
    end

    test "self-loop suppressed when used digi callsign matches sender callsign" do
      # Sender sees its own callsign reflected in the digi path (corrupted
      # frame). Should be skipped.
      lookup = lookup_fn(%{@sender => {99.0, 99.0}})

      hops =
        PathParser.parse_hops(
          @sender,
          @sender_pos,
          "#{@sender}*",
          @heard_at,
          lookup
        )

      assert hops == []
    end

    test "haversine distance for DFW → K5GVL-10 is approximately 88.4 km" do
      lookup = lookup_fn(%{"K5GVL-10" => {32.95, -96.10}})

      [hop] =
        PathParser.parse_hops(
          @sender,
          @sender_pos,
          "K5GVL-10*",
          @heard_at,
          lookup
        )

      assert abs(hop.distance_km - 88.4) < 1.0
    end
  end
end
