defmodule Aprsme.PacketTest do
  use Aprsme.DataCase, async: true

  alias Aprsme.Packet

  describe "extract_additional_data/2 with telemetry data" do
    test "converts string telemetry_vals to integers" do
      raw_packet = "W5ISP>APRS,TCPIP*:T#005,12.80,0.00,0.00,0.00,0.00,00000000"

      attrs = %{
        sender: "W5ISP",
        data_type: "telemetry",
        telemetry: %{
          seq: "005",
          vals: ["12.80", "0.00", "0.00", "0.00", "0.00"],
          bits: "00000000"
        }
      }

      result = Packet.extract_additional_data(attrs, raw_packet)

      assert result.data["telemetry_seq"] == 5
      # 12.80 rounds to 13
      assert result.data["telemetry_vals"] == [13, 0, 0, 0, 0]
      assert result.data["telemetry_bits"] == "00000000"
    end

    test "handles mixed integer and string telemetry_vals" do
      attrs = %{
        sender: "TEST",
        data_type: "telemetry",
        telemetry: %{
          seq: 10,
          vals: [180, "37.50", "0.00", 88, "164.75"],
          bits: "10101010"
        }
      }

      result = Packet.extract_additional_data(attrs, "test_packet")

      assert result.data["telemetry_seq"] == 10
      # Mixed types converted properly
      assert result.data["telemetry_vals"] == [180, 38, 0, 88, 165]
      assert result.data["telemetry_bits"] == "10101010"
    end

    test "handles float telemetry_vals" do
      attrs = %{
        sender: "TEST",
        data_type: "telemetry",
        telemetry: %{
          seq: "001",
          vals: [12.8, 37.2, 0.1, 88.9, 164.5]
        }
      }

      result = Packet.extract_additional_data(attrs, "test_packet")

      assert result.data["telemetry_seq"] == 1
      # Floats rounded to integers
      assert result.data["telemetry_vals"] == [13, 37, 0, 89, 165]
    end

    test "handles invalid telemetry_vals gracefully" do
      attrs = %{
        sender: "TEST",
        data_type: "telemetry",
        telemetry: %{
          seq: "002",
          vals: ["invalid", nil, "", "12.5", :atom]
        }
      }

      result = Packet.extract_additional_data(attrs, "test_packet")

      assert result.data["telemetry_seq"] == 2
      # Invalid values become 0
      assert result.data["telemetry_vals"] == [0, 0, 0, 13, 0]
    end

    test "handles missing telemetry data" do
      attrs = %{
        sender: "TEST",
        data_type: "position"
      }

      result = Packet.extract_additional_data(attrs, "test_packet")

      assert get_in(result, [:data, "telemetry_seq"]) == nil
      assert get_in(result, [:data, "telemetry_vals"]) == nil
      assert get_in(result, [:data, "telemetry_bits"]) == nil
    end
  end

  describe "comment cleaning" do
    test "strips weather data from weather packet comments" do
      attrs = %{
        sender: "TEST",
        data_type: "weather",
        data_extended: %{
          comment: "007/000g000t054r000p001P001b10052h90eMB61",
          data_type: :weather,
          wx: %{temperature: 54, humidity: 90, wind_speed: 0},
          latitude: 36.124,
          longitude: -75.723
        }
      }

      result = Packet.extract_additional_data(attrs, "test_packet")
      assert result[:comment] == "eMB61"
    end

    test "strips weather data with luminosity and preserves trailing comment" do
      attrs = %{
        sender: "TEST",
        data_type: "weather",
        data_extended: %{
          comment: "225/004g009t075r000p000h61b10206Plano, TX weather",
          data_type: :weather,
          wx: %{temperature: 75, humidity: 61, wind_speed: 4},
          latitude: 32.0,
          longitude: -96.0
        }
      }

      result = Packet.extract_additional_data(attrs, "test_packet")
      assert result[:comment] == "Plano, TX weather"
    end

    test "strips weather data with snow and luminosity fields" do
      attrs = %{
        sender: "TEST",
        data_type: "weather",
        data_extended: %{
          comment: "082/002g005t074r000p000P000b10099h58L708eMB63",
          data_type: :weather,
          wx: %{temperature: 74, humidity: 58, wind_speed: 2},
          latitude: 30.0,
          longitude: -90.0
        }
      }

      result = Packet.extract_additional_data(attrs, "test_packet")
      assert result[:comment] == "eMB63"
    end

    test "handles weather comment that is all weather data" do
      attrs = %{
        sender: "TEST",
        data_type: "weather",
        data_extended: %{
          comment: "_180/010g015t072r000p000h45b10132s005",
          data_type: :weather,
          wx: %{temperature: 72, humidity: 45, wind_speed: 10},
          latitude: 32.0,
          longitude: -96.0
        }
      }

      result = Packet.extract_additional_data(attrs, "test_packet")
      assert result[:comment] in [nil, ""]
    end

    test "strips positionless weather format (cXXXsXXX)" do
      attrs = %{
        sender: "TEST",
        data_type: "weather",
        data_extended: %{
          comment: "c011s000g000t077r000P000L000h95b10080GW2000C MaxGust:1.1mph",
          data_type: :weather,
          wx: %{temperature: 77, humidity: 95},
          latitude: 6.0,
          longitude: 79.0
        }
      }

      result = Packet.extract_additional_data(attrs, "test_packet")
      assert result[:comment] == "GW2000C MaxGust:1.1mph"
    end

    test "strips RNG data from comments" do
      attrs = %{
        sender: "TEST",
        data_type: "position",
        data_extended: %{
          comment: "RNG0050 2m Voice 145.57500MHz",
          latitude: 61.0,
          longitude: 14.0,
          radiorange: "0050"
        }
      }

      result = Packet.extract_additional_data(attrs, "test_packet")
      assert result[:comment] == "2m Voice 145.57500MHz"
    end

    test "does not modify non-weather, non-RNG comments" do
      attrs = %{
        sender: "TEST",
        data_type: "position",
        data_extended: %{
          comment: "70cm Voice (D-Star) 447.12500MHz -3.0000MHz",
          latitude: 46.0,
          longitude: -94.0
        }
      }

      result = Packet.extract_additional_data(attrs, "test_packet")
      assert result[:comment] == "70cm Voice (D-Star) 447.12500MHz -3.0000MHz"
    end

    test "handles weather data with spaces for missing values" do
      attrs = %{
        sender: "TEST",
        data_type: "weather",
        data_extended: %{
          comment: "215/004g012t046r   p   P000h  b10052KU2k",
          data_type: :weather,
          wx: %{temperature: 46, wind_speed: 4},
          latitude: 39.0,
          longitude: -84.0
        }
      }

      result = Packet.extract_additional_data(attrs, "test_packet")
      assert result[:comment] == "KU2k"
    end

    test "handles weather data with dots for missing values" do
      attrs = %{
        sender: "TEST",
        data_type: "weather",
        data_extended: %{
          comment: ".../...g...t...r...p...P...h..b.....My Station",
          data_type: :weather,
          wx: %{},
          latitude: 43.0,
          longitude: -79.0
        }
      }

      result = Packet.extract_additional_data(attrs, "test_packet")
      assert result[:comment] == "My Station"
    end

    test "handles negative temperature in weather data" do
      attrs = %{
        sender: "TEST",
        data_type: "weather",
        data_extended: %{
          comment: "000/000g005t-12r000p000h95b10200Cold Weather Station",
          data_type: :weather,
          wx: %{temperature: -12, humidity: 95, wind_speed: 0},
          latitude: 60.0,
          longitude: 25.0
        }
      }

      result = Packet.extract_additional_data(attrs, "test_packet")
      assert result[:comment] == "Cold Weather Station"
    end

    test "strips negative altitude from comments" do
      attrs = %{
        sender: "TEST",
        data_type: "position",
        data_extended: %{
          comment: "290/054/146.520MHz/A=-00043 https://aprsdroid.org/",
          latitude: 29.0,
          longitude: -83.0
        }
      }

      result = Packet.extract_additional_data(attrs, "test_packet")
      assert result[:comment] == "290/054/146.520MHz https://aprsdroid.org/"
    end

    test "strips combined PHG, altitude, and RNG from comment" do
      attrs = %{
        sender: "TEST",
        data_type: "position",
        data_extended: %{
          comment: "RNG0062/A=004756 2m Voice (D-Star) 145.68750MHz",
          latitude: 43.0,
          longitude: 11.0,
          radiorange: "0062"
        }
      }

      result = Packet.extract_additional_data(attrs, "test_packet")
      assert result[:comment] == "2m Voice (D-Star) 145.68750MHz"
    end

    test "does not false-match comment starting with P as weather field" do
      attrs = %{
        sender: "TEST",
        data_type: "position",
        data_extended: %{
          comment: "Please QSL via bureau",
          latitude: 40.0,
          longitude: -74.0
        }
      }

      result = Packet.extract_additional_data(attrs, "test_packet")
      assert result[:comment] == "Please QSL via bureau"
    end

    test "does not false-match comment starting with h as weather field" do
      attrs = %{
        sender: "TEST",
        data_type: "position",
        data_extended: %{
          comment: "https://aprs.fi",
          latitude: 40.0,
          longitude: -74.0
        }
      }

      result = Packet.extract_additional_data(attrs, "test_packet")
      assert result[:comment] == "https://aprs.fi"
    end

    test "returns nil for nil comment" do
      attrs = %{
        sender: "TEST",
        data_type: "position",
        data_extended: %{
          comment: nil,
          latitude: 40.0,
          longitude: -74.0
        }
      }

      result = Packet.extract_additional_data(attrs, "test_packet")
      assert is_nil(result[:comment])
    end

    test "returns nil for comment that becomes empty after stripping" do
      attrs = %{
        sender: "TEST",
        data_type: "position",
        data_extended: %{
          comment: "PHG5530/A=000680",
          latitude: 33.0,
          longitude: -96.0,
          phg: %{power: 25, height: 320, gain: 3, directivity: 0}
        }
      }

      result = Packet.extract_additional_data(attrs, "test_packet")
      assert is_nil(result[:comment])
    end
  end

  describe "PHG extraction" do
    test "extracts PHG from comment string" do
      attrs = %{
        sender: "TEST",
        data_type: "position",
        data_extended: %{
          comment: "PHG5530 Some comment",
          latitude: 33.0,
          longitude: -96.0
        }
      }

      result = Packet.extract_additional_data(attrs, "test_packet")

      # PHG5530: power=5^2=25, height=10*2^5=320, gain=3, dir=0*45=0
      assert result.data["phg_power"] == 25
      assert result.data["phg_height"] == 320
      assert result.data["phg_gain"] == 3
      assert result.data["phg_directivity"] == 0
    end

    test "calculates omnidirectional antenna (digit 9 = 360 degrees)" do
      attrs = %{
        sender: "TEST",
        data_type: "position",
        data_extended: %{
          comment: "PHG2369 My station",
          latitude: 40.0,
          longitude: -74.0
        }
      }

      result = Packet.extract_additional_data(attrs, "test_packet")

      # PHG2369: power=2^2=4, height=10*2^3=80, gain=6, dir=9→360
      assert result.data["phg_power"] == 4
      assert result.data["phg_height"] == 80
      assert result.data["phg_gain"] == 6
      assert result.data["phg_directivity"] == 360
    end

    test "calculates directional antenna" do
      attrs = %{
        sender: "TEST",
        data_type: "position",
        data_extended: %{
          comment: "PHG1064 Directional",
          latitude: 35.0,
          longitude: -80.0
        }
      }

      result = Packet.extract_additional_data(attrs, "test_packet")

      # PHG1064: power=1^2=1, height=10*2^0=10, gain=6, dir=4*45=180
      assert result.data["phg_power"] == 1
      assert result.data["phg_height"] == 10
      assert result.data["phg_gain"] == 6
      assert result.data["phg_directivity"] == 180
    end

    test "extracts PHG from data_extended map" do
      attrs = %{
        sender: "TEST",
        data_type: "position",
        data_extended: %{
          comment: "My station",
          latitude: 33.0,
          longitude: -96.0,
          phg: %{power: 25, height: 320, gain: 3, directivity: 0}
        }
      }

      result = Packet.extract_additional_data(attrs, "test_packet")

      assert result.data["phg_power"] == 25
      assert result.data["phg_height"] == 320
      assert result.data["phg_gain"] == 3
      assert result.data["phg_directivity"] == 0
    end

    test "extracts PHG from data_extended string format" do
      attrs = %{
        sender: "TEST",
        data_type: "position",
        data_extended: %{
          comment: "My station",
          latitude: 33.0,
          longitude: -96.0,
          phg: "5530"
        }
      }

      result = Packet.extract_additional_data(attrs, "test_packet")

      assert result.data["phg_power"] == 25
      assert result.data["phg_height"] == 320
      assert result.data["phg_gain"] == 3
      assert result.data["phg_directivity"] == 0
    end

    test "handles zero PHG values" do
      attrs = %{
        sender: "TEST",
        data_type: "position",
        data_extended: %{
          comment: "PHG0000 Minimal",
          latitude: 33.0,
          longitude: -96.0
        }
      }

      result = Packet.extract_additional_data(attrs, "test_packet")

      # PHG0000: power=0^2=0, height=10*2^0=10, gain=0, dir=0*45=0
      assert result.data["phg_power"] == 0
      assert result.data["phg_height"] == 10
      assert result.data["phg_gain"] == 0
      assert result.data["phg_directivity"] == 0
    end
  end

  describe "altitude extraction" do
    test "extracts standard altitude from comment" do
      attrs = %{
        sender: "TEST",
        data_type: "position",
        data_extended: %{
          comment: "Some info /A=001234 more info",
          latitude: 33.0,
          longitude: -96.0
        }
      }

      result = Packet.extract_additional_data(attrs, "test_packet")

      assert result[:altitude] == 1234.0
    end

    test "extracts altitude from data_extended map" do
      attrs = %{
        sender: "TEST",
        data_type: "position",
        data_extended: %{
          comment: "My station",
          latitude: 33.0,
          longitude: -96.0,
          altitude: 5280.0
        }
      }

      result = Packet.extract_additional_data(attrs, "test_packet")

      assert result[:altitude] == 5280.0
    end

    test "extracts zero altitude" do
      attrs = %{
        sender: "TEST",
        data_type: "position",
        data_extended: %{
          comment: "/A=000000 ground level",
          latitude: 33.0,
          longitude: -96.0
        }
      }

      result = Packet.extract_additional_data(attrs, "test_packet")

      assert result[:altitude] == 0.0
    end

    test "returns nil when no altitude present" do
      attrs = %{
        sender: "TEST",
        data_type: "position",
        data_extended: %{
          comment: "Just a plain comment",
          latitude: 33.0,
          longitude: -96.0
        }
      }

      result = Packet.extract_additional_data(attrs, "test_packet")

      assert is_nil(result[:altitude])
    end

    test "prefers data_extended altitude over comment-extracted altitude" do
      attrs = %{
        sender: "TEST",
        data_type: "position",
        data_extended: %{
          comment: "/A=001000 info",
          latitude: 33.0,
          longitude: -96.0,
          altitude: 9999.0
        }
      }

      result = Packet.extract_additional_data(attrs, "test_packet")

      assert result[:altitude] == 9999.0
    end
  end

  describe "changeset/2 course normalization" do
    test "keeps a valid course value untouched" do
      attrs = %{
        sender: "K1ABC",
        base_callsign: "K1ABC",
        ssid: "0",
        destination: "APRS",
        path: "WIDE1-1",
        raw_packet: "K1ABC>APRS:test",
        data_type: "position",
        course: 180
      }

      changeset = Packet.changeset(%Packet{}, attrs)
      assert Ecto.Changeset.get_field(changeset, :course) == 180
    end

    test "normalizes out-of-range course value to 0" do
      attrs = %{
        sender: "K1ABC",
        base_callsign: "K1ABC",
        ssid: "0",
        destination: "APRS",
        path: "WIDE1-1",
        raw_packet: "K1ABC>APRS:test",
        data_type: "position",
        # 360 is out of [0,359]; integer guard branch normalises to 0.
        course: 360
      }

      changeset = Packet.changeset(%Packet{}, attrs)
      assert Ecto.Changeset.get_field(changeset, :course) == 0
    end

    test "leaves non-integer course (after Ecto cast) alone" do
      # Ecto's :integer cast will convert valid strings to integers; passing a
      # non-castable value lands the field in :errors instead — exercises the
      # `_ -> changeset` fallback in normalize_course.
      attrs = %{
        sender: "K1ABC",
        base_callsign: "K1ABC",
        ssid: "0",
        destination: "APRS",
        path: "WIDE1-1",
        raw_packet: "K1ABC>APRS:test",
        data_type: "position",
        course: "not-a-number"
      }

      changeset = Packet.changeset(%Packet{}, attrs)
      # Cast should error on course; normalize_course leaves it as-is.
      refute changeset.valid?
    end
  end

  describe "changeset/2 wind_direction normalization" do
    test "keeps a valid wind_direction value untouched" do
      attrs = %{
        sender: "WX1",
        base_callsign: "WX1",
        ssid: "0",
        destination: "APRS",
        path: "WIDE1-1",
        raw_packet: "WX1>APRS:weather",
        data_type: "position",
        wind_direction: 90
      }

      changeset = Packet.changeset(%Packet{}, attrs)
      assert Ecto.Changeset.get_field(changeset, :wind_direction) == 90
    end

    test "wraps wind_direction 360 to 0" do
      attrs = %{
        sender: "WX1",
        base_callsign: "WX1",
        ssid: "0",
        destination: "APRS",
        path: "WIDE1-1",
        raw_packet: "WX1>APRS:weather",
        data_type: "position",
        wind_direction: 360
      }

      changeset = Packet.changeset(%Packet{}, attrs)
      assert Ecto.Changeset.get_field(changeset, :wind_direction) == 0
    end

    test "normalizes negative wind_direction to 0" do
      attrs = %{
        sender: "WX1",
        base_callsign: "WX1",
        ssid: "0",
        destination: "APRS",
        path: "WIDE1-1",
        raw_packet: "WX1>APRS:weather",
        data_type: "position",
        wind_direction: -45
      }

      changeset = Packet.changeset(%Packet{}, attrs)
      assert Ecto.Changeset.get_field(changeset, :wind_direction) == 0
    end

    test "normalizes wind_direction > 360 to 0" do
      attrs = %{
        sender: "WX1",
        base_callsign: "WX1",
        ssid: "0",
        destination: "APRS",
        path: "WIDE1-1",
        raw_packet: "WX1>APRS:weather",
        data_type: "position",
        wind_direction: 720
      }

      changeset = Packet.changeset(%Packet{}, attrs)
      assert Ecto.Changeset.get_field(changeset, :wind_direction) == 0
    end
  end

  defmodule FakeExt do
    @moduledoc false
    defstruct [:symbol_code, :symbol_table_id, :comment, :latitude, :longitude]
  end

  describe "extract_additional_data/2 with struct data_extended" do
    test "extracts from a non-MicE struct by converting it to a map" do
      # extract_from_map/1 has a struct clause that does Map.from_struct |>
      # extract_from_map. We exercise it by passing in a plain struct (not
      # MicE/ParseError), which falls through to the struct-conversion clause.
      attrs = %{
        sender: "STRUCTPKT",
        data_type: "position",
        data_extended: %FakeExt{
          symbol_code: ">",
          symbol_table_id: "/",
          comment: "From a struct",
          latitude: 33.0,
          longitude: -96.0
        }
      }

      result = Packet.extract_additional_data(attrs, "STRUCTPKT>APRS:test")
      assert result[:symbol_code] == ">"
      assert result[:symbol_table_id] == "/"
      assert result[:comment] == "From a struct"
    end
  end
end
