-- v020_features: Lagrange point support
-- Tests Sun-planet, Earth-Moon, planetary moon Lagrange points,
-- Hill radius, zone radius, DE fallback, and input validation.

CREATE EXTENSION IF NOT EXISTS pg_orrery;

-- Reference observer: Greenwich, UK
\set obs '''(51.4769,-0.0005,0)'''

-- Reference time: J2000 epoch (2000-01-01 12:00:00 UTC)
\set t '''2000-01-01 12:00:00+00'''

-- ============================================================
-- Sun-Earth L1/L2: should be ~0.01 AU from Earth (~1.5 million km)
-- SOHO is at L1, JWST at L2.
-- ============================================================

-- L1 heliocentric: should be close to Earth's heliocentric (~1 AU from Sun)
SELECT
    round(helio_distance(lagrange_heliocentric(3, 1, :t ::timestamptz))::numeric, 2) AS sun_dist_au;

-- L2 heliocentric: also ~1 AU from Sun, slightly further than L1
SELECT
    round(helio_distance(lagrange_heliocentric(3, 2, :t ::timestamptz))::numeric, 2) AS sun_dist_au;

-- L1 between Sun and Earth (closer to Sun than L2)
SELECT
    helio_distance(lagrange_heliocentric(3, 1, :t ::timestamptz))
    <
    helio_distance(lagrange_heliocentric(3, 2, :t ::timestamptz))
    AS l1_closer_than_l2;

-- ============================================================
-- Sun-Jupiter L4/L5: ~60 degrees from Jupiter, ~5.2 AU from Sun
-- These are the Trojan asteroid zones.
-- ============================================================

-- L4/L5 should be ~5.2 AU from Sun
SELECT
    round(helio_distance(lagrange_heliocentric(5, 4, :t ::timestamptz))::numeric, 1) AS l4_sun_dist;

SELECT
    round(helio_distance(lagrange_heliocentric(5, 5, :t ::timestamptz))::numeric, 1) AS l5_sun_dist;

-- L4 and L5 equidistant from Sun (within 0.001 AU)
SELECT
    abs(
        helio_distance(lagrange_heliocentric(5, 4, :t ::timestamptz))
        -
        helio_distance(lagrange_heliocentric(5, 5, :t ::timestamptz))
    ) < 0.001 AS l4_l5_equidistant;

-- ============================================================
-- Earth-Moon L1: ~326,000 km from Earth
-- ============================================================

-- lunar_lagrange_equatorial returns distance in km
SELECT
    round(eq_distance(lunar_lagrange_equatorial(1, :t ::timestamptz))::numeric, -3)
    BETWEEN 300000 AND 360000 AS em_l1_in_range;

-- ============================================================
-- lagrange_observe returns valid az/el
-- ============================================================

SELECT
    topo_elevation(lagrange_observe(3, 2, :obs ::observer, :t ::timestamptz))
    BETWEEN -90 AND 90 AS valid_elevation;

-- lagrange_equatorial returns valid RA/Dec
SELECT
    eq_ra(lagrange_equatorial(3, 1, :t ::timestamptz)) BETWEEN 0 AND 24 AS valid_ra,
    eq_dec(lagrange_equatorial(3, 1, :t ::timestamptz)) BETWEEN -90 AND 90 AS valid_dec;

-- ============================================================
-- lagrange_distance self-test: L-point distance to itself ≈ 0
-- ============================================================

SELECT
    round(lagrange_distance(
        5, 4,
        lagrange_heliocentric(5, 4, :t ::timestamptz),
        :t ::timestamptz
    )::numeric, 10) AS self_distance;

-- ============================================================
-- Hill radius
-- ============================================================

-- Jupiter Hill radius ~0.35 AU
SELECT
    round(hill_radius(5, :t ::timestamptz)::numeric, 2)
    BETWEEN 0.30 AND 0.40 AS jupiter_hill_ok;

-- Earth Hill radius ~0.01 AU
SELECT
    round(hill_radius(3, :t ::timestamptz)::numeric, 3)
    BETWEEN 0.008 AND 0.012 AS earth_hill_ok;

-- Lunar Hill radius (much smaller, AU)
SELECT
    hill_radius_lunar(:t ::timestamptz) > 0 AS lunar_hill_positive;

-- ============================================================
-- Zone radius
-- ============================================================

SELECT
    lagrange_zone_radius(5, 4, :t ::timestamptz) > 0 AS jup_l4_zone_positive;

SELECT
    lagrange_zone_radius(5, 1, :t ::timestamptz) > 0 AS jup_l1_zone_positive;

-- ============================================================
-- Convenience functions
-- ============================================================

-- lagrange_mass_ratio returns small positive number
SELECT
    lagrange_mass_ratio(5) > 0 AND lagrange_mass_ratio(5) < 0.01 AS jupiter_mu_ok;

SELECT
    lagrange_mass_ratio(3) > 0 AND lagrange_mass_ratio(3) < 0.001 AS earth_mu_ok;

-- lagrange_point_name
SELECT lagrange_point_name(1) AS l1_name;
SELECT lagrange_point_name(5) AS l5_name;

-- ============================================================
-- All planets produce valid results
-- ============================================================

SELECT body_id,
       round(helio_distance(lagrange_heliocentric(body_id, 1, :t ::timestamptz))::numeric, 2) AS sun_dist_au
FROM generate_series(1, 8) AS body_id
ORDER BY body_id;

-- ============================================================
-- Planetary moon Lagrange points
-- ============================================================

-- Galilean: Io L4 (body=0, point=4)
SELECT
    eq_ra(galilean_lagrange_equatorial(0, 4, :t ::timestamptz)) BETWEEN 0 AND 24
    AS io_l4_valid_ra;

-- Saturn: Titan L1 (body=5, point=1)
SELECT
    eq_ra(saturn_moon_lagrange_equatorial(5, 1, :t ::timestamptz)) BETWEEN 0 AND 24
    AS titan_l1_valid_ra;

-- Uranus: Titania L2 (body=3, point=2)
SELECT
    eq_ra(uranus_moon_lagrange_equatorial(3, 2, :t ::timestamptz)) BETWEEN 0 AND 24
    AS titania_l2_valid_ra;

-- Mars: Phobos L5 (body=0, point=5)
SELECT
    eq_ra(mars_moon_lagrange_equatorial(0, 5, :t ::timestamptz)) BETWEEN 0 AND 24
    AS phobos_l5_valid_ra;

-- Galilean observe returns valid topocentric
SELECT
    topo_elevation(galilean_lagrange_observe(2, 3, :obs ::observer, :t ::timestamptz))
    BETWEEN -90 AND 90 AS ganymede_l3_valid_el;

-- ============================================================
-- DE fallback (no DE loaded, should produce same results as VSOP87)
-- ============================================================

SELECT
    round(helio_distance(lagrange_heliocentric_de(3, 1, :t ::timestamptz))::numeric, 4) AS de_l1_dist;

SELECT
    round(helio_distance(lagrange_heliocentric(3, 1, :t ::timestamptz))::numeric, 4) AS vsop_l1_dist;

-- DE hill_radius fallback
SELECT
    round(hill_radius_de(5, :t ::timestamptz)::numeric, 4) =
    round(hill_radius(5, :t ::timestamptz)::numeric, 4)
    AS hill_de_matches_vsop;

-- ============================================================
-- Tighter L1/L2 precision (4 decimal places)
-- ============================================================

SELECT
    round(helio_distance(lagrange_heliocentric(3, 1, :t ::timestamptz))::numeric, 4) AS earth_l1_dist;

SELECT
    round(helio_distance(lagrange_heliocentric(3, 2, :t ::timestamptz))::numeric, 4) AS earth_l2_dist;

-- L1 and L2 bracket Earth's heliocentric distance
SELECT
    helio_distance(lagrange_heliocentric(3, 1, :t ::timestamptz))
    <
    helio_distance(planet_heliocentric(3, :t ::timestamptz))
    AND
    helio_distance(planet_heliocentric(3, :t ::timestamptz))
    <
    helio_distance(lagrange_heliocentric(3, 2, :t ::timestamptz))
    AS l1_earth_l2_ordering;

-- ============================================================
-- lagrange_distance_oe — Ceres distance from Jupiter L4
-- Ceres orbits at ~2.77 AU, Jupiter L4 at ~5.2 AU, so distance > 2 AU
-- ============================================================

SELECT
    round(lagrange_distance_oe(
        5, 4,
        oe_from_mpc('00001    3.33  0.12 K24AM  60.07966  73.42937   80.26860  10.58664  0.0789126  0.21406048   2.7660961  0 MPO838504  8738 115 1801-2024 0.65 M-v 30k MPCLINUX   0000      (1) Ceres              20240825'),
        :t ::timestamptz
    )::numeric, 2) AS ceres_jup_l4_dist;

-- Distance should be positive and > 2 AU (main belt vs Trojan zone)
SELECT
    lagrange_distance_oe(
        5, 4,
        oe_from_mpc('00001    3.33  0.12 K24AM  60.07966  73.42937   80.26860  10.58664  0.0789126  0.21406048   2.7660961  0 MPO838504  8738 115 1801-2024 0.65 M-v 30k MPCLINUX   0000      (1) Ceres              20240825'),
        :t ::timestamptz
    ) > 2.0 AS ceres_far_from_trojan;

-- ============================================================
-- DE fallback for planetary moon Lagrange functions
-- ============================================================

SELECT
    round(eq_ra(galilean_lagrange_equatorial_de(0, 4, :t ::timestamptz))::numeric, 4) =
    round(eq_ra(galilean_lagrange_equatorial(0, 4, :t ::timestamptz))::numeric, 4)
    AS galilean_de_fallback_matches;

SELECT
    round(eq_ra(saturn_moon_lagrange_equatorial_de(5, 1, :t ::timestamptz))::numeric, 4) =
    round(eq_ra(saturn_moon_lagrange_equatorial(5, 1, :t ::timestamptz))::numeric, 4)
    AS saturn_de_fallback_matches;

-- ============================================================
-- Input validation
-- ============================================================

-- Bad body_id
SELECT lagrange_heliocentric(0, 1, :t ::timestamptz);  -- Sun not valid
SELECT lagrange_heliocentric(9, 1, :t ::timestamptz);  -- body 9 invalid

-- Bad point_id
SELECT lagrange_heliocentric(3, 0, :t ::timestamptz);  -- point 0 invalid
SELECT lagrange_heliocentric(3, 6, :t ::timestamptz);  -- point 6 invalid

-- Bad lunar point_id
SELECT lunar_lagrange_equatorial(0, :t ::timestamptz);  -- point 0 invalid
SELECT lunar_lagrange_equatorial(6, :t ::timestamptz);  -- point 6 invalid

-- Bad planetary moon body_id
SELECT galilean_lagrange_equatorial(4, 1, :t ::timestamptz);  -- Galilean 4 invalid
SELECT saturn_moon_lagrange_equatorial(8, 1, :t ::timestamptz);  -- Saturn 8 invalid
SELECT uranus_moon_lagrange_equatorial(5, 1, :t ::timestamptz);  -- Uranus 5 invalid
SELECT mars_moon_lagrange_equatorial(2, 1, :t ::timestamptz);  -- Mars 2 invalid

-- lagrange_mass_ratio bad body
SELECT lagrange_mass_ratio(0);
SELECT lagrange_mass_ratio(9);

-- lagrange_point_name bad id
SELECT lagrange_point_name(0);
SELECT lagrange_point_name(6);
