defmodule Microwaveprop.AprsTest do
  use ExUnit.Case, async: false

  alias Ecto.Adapters.SQL
  alias Ecto.Adapters.SQL.Sandbox
  alias Microwaveprop.Aprs
  alias Microwaveprop.AprsRepo

  setup do
    pid = Sandbox.start_owner!(AprsRepo, shared: true)
    on_exit(fn -> Sandbox.stop_owner(pid) end)
    :ok
  end

  defp insert_packet(opts) do
    fields =
      Keyword.merge(
        [
          sender: "TEST-A",
          base_callsign: "TEST-A",
          lat: 33.0,
          lon: -97.0,
          path: "WIDE1*",
          received_at: NaiveDateTime.utc_now(),
          has_position: true,
          is_item: false,
          is_object: false
        ],
        opts
      )

    SQL.query!(
      AprsRepo,
      """
      INSERT INTO packets
        (id, sender, base_callsign, lat, lon, path, received_at,
         has_position, is_item, is_object, inserted_at, updated_at)
      VALUES
        (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, now(), now())
      """,
      [
        fields[:sender],
        fields[:base_callsign],
        fields[:lat],
        fields[:lon],
        fields[:path],
        fields[:received_at],
        fields[:has_position],
        fields[:is_item],
        fields[:is_object]
      ]
    )

    :ok
  end

  describe "recent_packets_with_paths/1" do
    test "filters out is_item, is_object, has_position=false, empty path, and null lat/lon" do
      now = NaiveDateTime.utc_now()
      since = DateTime.add(DateTime.utc_now(), -3600, :second)

      # 1 valid row
      :ok = insert_packet(sender: "TEST-VALID", path: "K5GVL-10*,WIDE1-1", received_at: now)

      # 7 rejected rows
      :ok = insert_packet(sender: "TEST-ITEM", is_item: true, received_at: now)
      :ok = insert_packet(sender: "TEST-OBJECT", is_object: true, received_at: now)
      :ok = insert_packet(sender: "TEST-NOPOS", has_position: false, received_at: now)
      :ok = insert_packet(sender: "TEST-EMPTYPATH", path: "", received_at: now)
      :ok = insert_packet(sender: "TEST-NULLPATH", path: nil, received_at: now)
      :ok = insert_packet(sender: "TEST-NULLLAT", lat: nil, received_at: now)
      :ok = insert_packet(sender: "TEST-NULLLON", lon: nil, received_at: now)

      rows = Aprs.recent_packets_with_paths(since: since)

      assert length(rows) == 1
      assert hd(rows).sender == "TEST-VALID"
    end

    test "honors :since" do
      now = DateTime.utc_now()
      recent = NaiveDateTime.add(NaiveDateTime.utc_now(), -1800, :second)
      old = NaiveDateTime.add(NaiveDateTime.utc_now(), -5400, :second)

      :ok = insert_packet(sender: "TEST-RECENT", received_at: recent)
      :ok = insert_packet(sender: "TEST-OLD", received_at: old)

      since = DateTime.add(now, -3600, :second)
      rows = Aprs.recent_packets_with_paths(since: since)

      senders = Enum.map(rows, & &1.sender)
      assert "TEST-RECENT" in senders
      refute "TEST-OLD" in senders
    end

    test "honors :limit" do
      since = DateTime.add(DateTime.utc_now(), -3600, :second)

      Enum.each(1..5, fn i ->
        # Stagger by seconds so ordering is deterministic.
        ts = NaiveDateTime.add(NaiveDateTime.utc_now(), -i, :second)
        :ok = insert_packet(sender: "TEST-L#{i}", received_at: ts)
      end)

      rows = Aprs.recent_packets_with_paths(since: since, limit: 2)
      assert length(rows) == 2
    end

    test "returns rows in received_at ascending order" do
      since = DateTime.add(DateTime.utc_now(), -3600, :second)
      base = NaiveDateTime.utc_now()

      :ok = insert_packet(sender: "TEST-MID", received_at: NaiveDateTime.add(base, -120, :second))
      :ok = insert_packet(sender: "TEST-OLDEST", received_at: NaiveDateTime.add(base, -300, :second))
      :ok = insert_packet(sender: "TEST-NEWEST", received_at: NaiveDateTime.add(base, -10, :second))

      rows =
        since
        |> then(&Aprs.recent_packets_with_paths(since: &1))
        |> Enum.filter(&String.starts_with?(&1.sender, "TEST-"))

      senders = Enum.map(rows, & &1.sender)
      assert senders == ["TEST-OLDEST", "TEST-MID", "TEST-NEWEST"]
    end

    test "decodes Decimal lat/lon as floats" do
      since = DateTime.add(DateTime.utc_now(), -3600, :second)
      :ok = insert_packet(sender: "TEST-FLOAT", lat: 33.123, lon: -97.456)

      rows = Aprs.recent_packets_with_paths(since: since)
      row = Enum.find(rows, &(&1.sender == "TEST-FLOAT"))

      assert is_float(row.lat)
      assert is_float(row.lon)
      assert_in_delta row.lat, 33.123, 0.0001
      assert_in_delta row.lon, -97.456, 0.0001
    end

    test "decodes received_at as a UTC DateTime" do
      since = DateTime.add(DateTime.utc_now(), -3600, :second)
      :ok = insert_packet(sender: "TEST-TZ")

      rows = Aprs.recent_packets_with_paths(since: since)
      row = Enum.find(rows, &(&1.sender == "TEST-TZ"))

      assert %DateTime{} = row.received_at
      assert row.received_at.time_zone == "Etc/UTC"
    end
  end

  describe "station_positions/1" do
    test "returns the most recent fix per callsign and omits unknown callsigns" do
      base = NaiveDateTime.utc_now()

      :ok =
        insert_packet(
          sender: "TEST-A",
          base_callsign: "TEST-A",
          lat: 30.0,
          lon: -90.0,
          received_at: NaiveDateTime.add(base, -3600, :second)
        )

      :ok =
        insert_packet(
          sender: "TEST-A",
          base_callsign: "TEST-A",
          lat: 31.0,
          lon: -91.0,
          received_at: NaiveDateTime.add(base, -1800, :second)
        )

      :ok =
        insert_packet(
          sender: "TEST-A",
          base_callsign: "TEST-A",
          lat: 32.5,
          lon: -92.5,
          received_at: NaiveDateTime.add(base, -60, :second)
        )

      :ok =
        insert_packet(
          sender: "TEST-B",
          base_callsign: "TEST-B",
          lat: 40.0,
          lon: -100.0,
          received_at: NaiveDateTime.add(base, -300, :second)
        )

      result = Aprs.station_positions(["TEST-A", "TEST-B", "TEST-C"])

      assert result |> Map.keys() |> Enum.sort() == ["TEST-A", "TEST-B"]

      {lat_a, lon_a, heard_a} = result["TEST-A"]
      assert_in_delta lat_a, 32.5, 0.0001
      assert_in_delta lon_a, -92.5, 0.0001
      assert %DateTime{} = heard_a
      assert heard_a.time_zone == "Etc/UTC"

      {lat_b, lon_b, _heard_b} = result["TEST-B"]
      assert_in_delta lat_b, 40.0, 0.0001
      assert_in_delta lon_b, -100.0, 0.0001

      refute Map.has_key?(result, "TEST-C")
    end

    test "empty list short-circuits to %{} without querying" do
      # Stop the sandbox owner so a real query would crash with NoConnectionError.
      Sandbox.checkin(AprsRepo)
      assert Aprs.station_positions([]) == %{}
    end
  end
end
