defmodule Microwaveprop.PskrTest do
  use ExUnit.Case, async: true

  alias Microwaveprop.Pskr

  @sample %{
    "sq" => 30_142_870_791,
    "f" => 144_174_000,
    "md" => "FT8",
    "rp" => -5,
    "t" => 1_662_407_712,
    "t_tx" => 1_662_407_697,
    "sc" => "K5ABC",
    "sl" => "EM12kl",
    "rc" => "K7XYZ",
    "rl" => "DM43st",
    "sa" => 291,
    "ra" => 291,
    "b" => "2m"
  }

  describe "parse_spot/1" do
    test "preserves full subsquare precision when reported" do
      json = Jason.encode!(@sample)

      assert {:ok, spot} = Pskr.parse_spot(json)

      assert spot.band == "2m"
      assert spot.mode == "FT8"
      assert spot.snr_db == -5
      assert spot.sender_grid == "EM12KL"
      assert spot.receiver_grid == "DM43ST"
      assert spot.sender_country == 291
      assert spot.receiver_country == 291
      assert %DateTime{} = spot.transmit_time
      assert DateTime.to_unix(spot.transmit_time) == 1_662_407_697
    end

    test "rejects 4-char grids on either end (insufficient precision)" do
      # A 4-char locator is ~70 km × 100 km — the midpoint and
      # distance error swamps any HRRR cell match (3 km native).
      # Drop the spot rather than store a calibration sample whose
      # geometry is meaningless.
      sender_short = @sample |> Map.put("sl", "EM12") |> Jason.encode!()
      receiver_short = @sample |> Map.put("rl", "DM43") |> Jason.encode!()

      assert {:error, _} = Pskr.parse_spot(sender_short)
      assert {:error, _} = Pskr.parse_spot(receiver_short)
    end

    test "preserves extended-square (8-char) precision" do
      json = @sample |> Map.merge(%{"sl" => "EM12kl37", "rl" => "DM43st99"}) |> Jason.encode!()

      assert {:ok, spot} = Pskr.parse_spot(json)
      assert spot.sender_grid == "EM12KL37"
      assert spot.receiver_grid == "DM43ST99"
    end

    test "rejects odd-length grids (truncated 5-char)" do
      json = @sample |> Map.put("sl", "EM12k") |> Jason.encode!()
      assert {:error, _} = Pskr.parse_spot(json)
    end

    test "uses :t when :t_tx is missing" do
      json = @sample |> Map.delete("t_tx") |> Jason.encode!()

      assert {:ok, spot} = Pskr.parse_spot(json)
      assert DateTime.to_unix(spot.transmit_time) == 1_662_407_712
    end

    test "rejects payloads without grids" do
      json = @sample |> Map.delete("sl") |> Jason.encode!()
      assert {:error, _} = Pskr.parse_spot(json)
    end

    test "rejects malformed JSON" do
      assert {:error, _} = Pskr.parse_spot("not-json")
    end
  end

  describe "path_key/1" do
    test "buckets to the top of the hour using the full grid as reported" do
      {:ok, spot} = Pskr.parse_spot(Jason.encode!(@sample))
      {hour, band, snd, rcv} = Pskr.path_key(spot)

      assert band == "2m"
      assert snd == "EM12KL"
      assert rcv == "DM43ST"
      # 1_662_407_697 → 2022-09-05T19:54:57Z → bucket at 19:00:00Z
      assert hour == ~U[2022-09-05 19:00:00Z]
    end
  end

  describe "merge_spot/2" do
    test "initializes accumulator from a single spot" do
      {:ok, spot} = Pskr.parse_spot(Jason.encode!(@sample))

      acc = Pskr.merge_spot(nil, spot)

      assert acc.spot_count == 1
      assert acc.max_snr_db == -5
      assert acc.min_snr_db == -5
      assert acc.modes == ["FT8"]
      assert acc.sender_grid == "EM12KL"
      assert acc.receiver_grid == "DM43ST"
      assert is_float(acc.distance_km)
      assert acc.distance_km > 0
      # Midpoint should land between the two grid centers
      assert acc.midpoint_lat > min(acc.sender_lat, acc.receiver_lat)
      assert acc.midpoint_lat < max(acc.sender_lat, acc.receiver_lat)
    end

    test "increments count and tracks SNR envelope across spots" do
      {:ok, first} = Pskr.parse_spot(Jason.encode!(@sample))
      {:ok, louder} = Pskr.parse_spot(Jason.encode!(%{@sample | "rp" => -1, "md" => "FT4"}))
      {:ok, quieter} = Pskr.parse_spot(Jason.encode!(%{@sample | "rp" => -18}))

      acc =
        nil
        |> Pskr.merge_spot(first)
        |> Pskr.merge_spot(louder)
        |> Pskr.merge_spot(quieter)

      assert acc.spot_count == 3
      assert acc.max_snr_db == -1
      assert acc.min_snr_db == -18
      assert Enum.sort(acc.modes) == ["FT4", "FT8"]
      assert DateTime.compare(acc.first_spot_at, acc.last_spot_at) in [:lt, :eq]
    end
  end
end
