defmodule Microwaveprop.MoonTest do
  @moduledoc """
  Tests for the low-precision lunar ephemeris.

  The underlying algorithm (Astronomical Almanac Sec. D.4) is accurate
  to ~0.3° on position and ~200 km on distance — tolerances chosen
  here reflect that.
  """
  use ExUnit.Case, async: true
  use ExUnitProperties

  alias Microwaveprop.Moon

  describe "julian_day/1" do
    test "J2000 epoch is 2_451_545.0 at 2000-01-01 12:00 UTC" do
      # By definition — the J2000 reference epoch.
      assert Moon.julian_day(~U[2000-01-01 12:00:00Z]) == 2_451_545.0
    end

    test "1992-04-12 00:00 UTC is Meeus Example 47.a (2_448_724.5)" do
      # Meeus, Astronomical Algorithms (2nd ed.) page 342.
      assert Moon.julian_day(~U[1992-04-12 00:00:00Z]) == 2_448_724.5
    end

    test "increments by 1 per UTC day" do
      assert_in_delta Moon.julian_day(~U[2024-01-02 00:00:00Z]) -
                        Moon.julian_day(~U[2024-01-01 00:00:00Z]),
                      1.0,
                      1.0e-9
    end

    test "increments by 0.5 per 12-hour step" do
      assert_in_delta Moon.julian_day(~U[2024-06-15 12:00:00Z]) -
                        Moon.julian_day(~U[2024-06-15 00:00:00Z]),
                      0.5,
                      1.0e-9
    end

    test "handles the January / February month-correction branch" do
      # Meeus's algorithm shifts Jan/Feb into the previous year's
      # month 13/14 — this test exercises that branch explicitly.
      assert_in_delta Moon.julian_day(~U[2024-02-29 00:00:00Z]) -
                        Moon.julian_day(~U[2024-02-28 00:00:00Z]),
                      1.0,
                      1.0e-9
    end

    property "monotonic in time" do
      check all(
              base_year <- integer(1900..2100),
              day_offset <- integer(0..364),
              delta_hours <- integer(1..72)
            ) do
        base = %DateTime{
          year: base_year,
          month: 1,
          day: 1,
          hour: 0,
          minute: 0,
          second: 0,
          microsecond: {0, 0},
          std_offset: 0,
          utc_offset: 0,
          zone_abbr: "UTC",
          time_zone: "Etc/UTC"
        }

        a = DateTime.add(base, day_offset * 86_400, :second)
        b = DateTime.add(a, delta_hours * 3600, :second)

        assert Moon.julian_day(a) < Moon.julian_day(b)
      end
    end
  end

  describe "geocentric/1" do
    test "distance stays inside the Earth-Moon perigee/apogee envelope" do
      # Min ~356_500 km, max ~406_700 km at the extremes over the
      # 21st century. Include a small margin for the simplified series.
      for iso <- ~w(2024-01-01T00:00:00Z 2024-06-15T12:00:00Z 2025-03-30T00:00:00Z 2030-11-11T00:00:00Z) do
        {:ok, dt, _} = DateTime.from_iso8601(iso)
        %{distance_km: d} = Moon.geocentric(dt)

        assert d > 350_000 and d < 410_000,
               "got distance #{d} km at #{iso}, outside the 350_000..410_000 envelope"
      end
    end

    test "declination is bounded by the Moon's ±28.6° extremes" do
      for iso <- ~w(2024-01-01T00:00:00Z 2024-06-15T12:00:00Z 2025-09-21T09:00:00Z) do
        {:ok, dt, _} = DateTime.from_iso8601(iso)
        %{dec: dec} = Moon.geocentric(dt)
        assert dec > -29.0 and dec < 29.0
      end
    end

    test "right ascension is always in [0, 360)" do
      for iso <- ~w(2024-01-01T00:00:00Z 2024-04-30T06:00:00Z 2024-09-12T18:00:00Z) do
        {:ok, dt, _} = DateTime.from_iso8601(iso)
        %{ra: ra} = Moon.geocentric(dt)
        assert ra >= 0.0 and ra < 360.0
      end
    end

    property "distance, RA, and declination all stay in their physical envelopes for any UTC across the next century" do
      check all(seconds_from_now <- integer(0..3_155_760_000)) do
        dt = DateTime.add(~U[2024-01-01 00:00:00Z], seconds_from_now, :second)
        g = Moon.geocentric(dt)

        assert g.distance_km > 340_000 and g.distance_km < 420_000
        assert g.dec > -30.0 and g.dec < 30.0
        assert g.ra >= 0.0 and g.ra < 360.0
      end
    end
  end

  describe "observer_position/3" do
    test "azimuth and elevation are always in their ranges" do
      for iso <- ~w(2024-01-01T00:00:00Z 2024-06-15T12:00:00Z) do
        {:ok, dt, _} = DateTime.from_iso8601(iso)
        # DFW, London, Sydney, North Pole.
        for {lat, lon} <- [{32.9, -97.0}, {51.5, 0.0}, {-33.9, 151.2}, {89.9, 0.0}] do
          p = Moon.observer_position(dt, lat, lon)
          assert p.azimuth >= 0.0 and p.azimuth < 360.0
          assert p.elevation >= -90.0 and p.elevation <= 90.0
          assert p.distance_km > 340_000 and p.distance_km < 420_000
        end
      end
    end

    test "distance matches geocentric/1 (topocentric correction skipped)" do
      # The module stubs topocentric slant range (< 0.02° error) so
      # observer distance equals geocentric distance.
      dt = ~U[2024-06-15 12:00:00Z]
      g = Moon.geocentric(dt)
      p = Moon.observer_position(dt, 32.9, -97.0)
      assert_in_delta p.distance_km, g.distance_km, 1.0e-6
    end

    property "azimuth always in [0, 360), elevation in [-90, 90]" do
      check all(
              seconds <- integer(0..3_155_760_000),
              lat <- float(min: -85.0, max: 85.0),
              lon <- float(min: -179.9, max: 179.9)
            ) do
        dt = DateTime.add(~U[2024-01-01 00:00:00Z], seconds, :second)
        p = Moon.observer_position(dt, lat, lon)
        assert p.azimuth >= 0.0 and p.azimuth < 360.0
        assert p.elevation >= -90.0 and p.elevation <= 90.0
      end
    end

    test "radial_velocity_km_s stays within the dominant orbital envelope ±1.5 km/s" do
      # The Earth-Moon distance changes at most by its orbital
      # eccentricity over half a month; instantaneously |dr/dt| peaks
      # near ±0.5 km/s on the orbital component. Adding observer
      # diurnal baseline motion (max ~0.5 km/s extra at equatorial
      # lat) keeps the envelope under ~1.5 km/s.
      for iso <- ~w(2024-01-01T00:00:00Z 2024-06-15T12:00:00Z 2025-09-21T09:00:00Z) do
        {:ok, dt, _} = DateTime.from_iso8601(iso)

        for {lat, lon} <- [{32.9, -97.0}, {51.5, 0.0}, {-33.9, 151.2}] do
          v = Moon.radial_velocity_km_s(dt, lat, lon)
          assert is_number(v)
          assert abs(v) < 1.5, "radial velocity #{v} km/s out of range at #{iso} / #{lat},#{lon}"
        end
      end
    end

    test "moon climbs through local zenith roughly every 24h 50m at CONUS lat" do
      # Sanity: the Moon's average transit (meridian) cadence is a
      # lunar day = 24 h 50 m. Take the max elevation across two
      # successive 25-hour windows at DFW and confirm both peaks
      # occur — i.e. the function isn't frozen.
      lat = 32.9
      lon = -97.0
      base = ~U[2024-06-01 00:00:00Z]

      peaks =
        for hour_offset <- 0..48 do
          dt = DateTime.add(base, hour_offset * 3600, :second)
          {dt, Moon.observer_position(dt, lat, lon).elevation}
        end

      els = Enum.map(peaks, &elem(&1, 1))
      max_el = Enum.max(els)
      min_el = Enum.min(els)

      # The Moon has to reach both a positive and a negative elevation
      # over 48 hours at 33°N regardless of orbital phase.
      assert max_el > 10.0
      assert min_el < -10.0
    end
  end
end
