defmodule Microwaveprop.Radio.MaidenheadPropertyTest do
  use ExUnit.Case, async: true
  use ExUnitProperties

  alias Microwaveprop.Radio.Maidenhead

  # Generators ---------------------------------------------------------

  # Upper-case field chars A-R (18 divisions, the only canonical output
  # from from_latlon/3).
  defp field_char, do: map(integer(0..17), &<<?A + &1>>)

  # Upper-case subsquare chars A-X (24 divisions). from_latlon/3 emits
  # lower-case, so we hold grids canonical in lower-case when assembling.
  defp subsquare_char, do: map(integer(0..23), &<<?a + &1>>)

  defp digit_char, do: map(integer(0..9), &<<?0 + &1>>)

  # Canonical 4-char grid: UPPER field + digits. to_latlon upcases, so
  # staying canonical here gives us exact round-trip equality.
  defp grid4 do
    gen all(
          f1 <- field_char(),
          f2 <- field_char(),
          d1 <- digit_char(),
          d2 <- digit_char()
        ) do
      f1 <> f2 <> d1 <> d2
    end
  end

  defp grid6 do
    gen all(
          g <- grid4(),
          s1 <- subsquare_char(),
          s2 <- subsquare_char()
        ) do
      g <> s1 <> s2
    end
  end

  defp grid8 do
    gen all(
          g <- grid6(),
          d1 <- digit_char(),
          d2 <- digit_char()
        ) do
      g <> d1 <> d2
    end
  end

  # Lat/lon that stay inside the representable field range. The top row
  # (lat = 90.0) and the wrap-around longitude (180.0) sit exactly on a
  # field boundary and land outside the A-R field set, so we stop just
  # short.
  defp latlon do
    gen all(
          lat <- float(min: -90.0, max: 89.999),
          lon <- float(min: -180.0, max: 179.999)
        ) do
      {lat, lon}
    end
  end

  # Properties ---------------------------------------------------------

  describe "round-trip from_latlon/3 → to_latlon/1" do
    property "precision 4: decoded center is within one grid cell of input" do
      # A 4-char grid covers 2° lon × 1° lat; the center must be within
      # half that distance of any point in the cell.
      check all({lat, lon} <- latlon()) do
        grid = Maidenhead.from_latlon(lat, lon, 4)
        assert {:ok, {dlat, dlon}} = Maidenhead.to_latlon(grid)
        assert abs(dlat - lat) <= 0.5 + 1.0e-9
        assert abs(dlon - lon) <= 1.0 + 1.0e-9
      end
    end

    property "precision 6: decoded center is within half a subsquare" do
      # 6-char subsquare is 5'×2.5' → 5/60 lon, 2.5/60 lat.
      check all({lat, lon} <- latlon()) do
        grid = Maidenhead.from_latlon(lat, lon, 6)
        assert {:ok, {dlat, dlon}} = Maidenhead.to_latlon(grid)
        assert abs(dlat - lat) <= 2.5 / 60 / 2 + 1.0e-9
        assert abs(dlon - lon) <= 5.0 / 60 / 2 + 1.0e-9
      end
    end

    property "precision 8: decoded center is within half an extended square" do
      check all({lat, lon} <- latlon()) do
        grid = Maidenhead.from_latlon(lat, lon, 8)
        assert {:ok, {dlat, dlon}} = Maidenhead.to_latlon(grid)
        assert abs(dlat - lat) <= 2.5 / 60 / 10 / 2 + 1.0e-9
        assert abs(dlon - lon) <= 5.0 / 60 / 10 / 2 + 1.0e-9
      end
    end
  end

  describe "round-trip to_latlon/1 → from_latlon/3" do
    property "precision 4 grids come back unchanged (after upcasing)" do
      check all(grid <- grid4()) do
        assert {:ok, {lat, lon}} = Maidenhead.to_latlon(grid)
        assert Maidenhead.from_latlon(lat, lon, 4) == String.upcase(grid)
      end
    end

    property "precision 6 grids come back unchanged" do
      # from_latlon/3 emits upper-case field/square and lower-case
      # subsquare, matching the canonical form our generator produces.
      check all(grid <- grid6()) do
        assert {:ok, {lat, lon}} = Maidenhead.to_latlon(grid)
        assert Maidenhead.from_latlon(lat, lon, 6) == grid
      end
    end

    property "precision 8 grids come back unchanged" do
      check all(grid <- grid8()) do
        assert {:ok, {lat, lon}} = Maidenhead.to_latlon(grid)
        assert Maidenhead.from_latlon(lat, lon, 8) == grid
      end
    end
  end

  describe "decoded bounds" do
    property "to_latlon output stays inside valid geographic range" do
      check all(grid <- grid8()) do
        assert {:ok, {lat, lon}} = Maidenhead.to_latlon(grid)
        assert lat >= -90.0 and lat <= 90.0
        assert lon >= -180.0 and lon <= 180.0
      end
    end
  end

  describe "valid?/1" do
    property "accepts every grid emitted by from_latlon/3" do
      check all(
              {lat, lon} <- latlon(),
              precision <- one_of([constant(4), constant(6), constant(8)])
            ) do
        assert Maidenhead.valid?(Maidenhead.from_latlon(lat, lon, precision))
      end
    end

    property "never crashes on arbitrary binary input" do
      check all(s <- string(:printable)) do
        assert is_boolean(Maidenhead.valid?(s))
      end
    end
  end
end
