# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""Accuracy tests for GCRS coordinate transformations, primarily to/from AltAz.

"""
import os
import warnings
from importlib import metadata

import pytest
import numpy as np
import erfa

from astropy import units as u
from astropy.tests.helper import assert_quantity_allclose as assert_allclose
from astropy.time import Time
from astropy.coordinates import (
    EarthLocation, get_sun, ICRS, GCRS, CIRS, ITRS, AltAz, HADec,
    PrecessedGeocentric, CartesianRepresentation, SkyCoord,
    CartesianDifferential, SphericalRepresentation, UnitSphericalRepresentation,
    HCRS, HeliocentricMeanEcliptic, TEME, TETE)
from astropy.coordinates.solar_system import _apparent_position_in_true_coordinates, get_body
from astropy.utils import iers
from astropy.utils.exceptions import AstropyWarning, AstropyDeprecationWarning
from astropy.utils.compat.optional_deps import HAS_JPLEPHEM

from astropy.coordinates.angle_utilities import golden_spiral_grid
from astropy.coordinates.builtin_frames.intermediate_rotation_transforms import (
    get_location_gcrs, tete_to_itrs_mat, gcrs_to_cirs_mat, cirs_to_itrs_mat)
from astropy.coordinates.builtin_frames.utils import get_jd12
from astropy.coordinates import solar_system_ephemeris
from astropy.units import allclose

CI = os.environ.get('CI', False) == "true"


def test_icrs_cirs():
    """
    Check a few cases of ICRS<->CIRS for consistency.

    Also includes the CIRS<->CIRS transforms at different times, as those go
    through ICRS
    """
    usph = golden_spiral_grid(200)
    dist = np.linspace(0., 1, len(usph)) * u.pc
    inod = ICRS(usph)
    iwd = ICRS(ra=usph.lon, dec=usph.lat, distance=dist)

    cframe1 = CIRS()
    cirsnod = inod.transform_to(cframe1)  # uses the default time
    # first do a round-tripping test
    inod2 = cirsnod.transform_to(ICRS())
    assert_allclose(inod.ra, inod2.ra)
    assert_allclose(inod.dec, inod2.dec)

    # now check that a different time yields different answers
    cframe2 = CIRS(obstime=Time('J2005'))
    cirsnod2 = inod.transform_to(cframe2)
    assert not allclose(cirsnod.ra, cirsnod2.ra, rtol=1e-8)
    assert not allclose(cirsnod.dec, cirsnod2.dec, rtol=1e-8)

    # parallax effects should be included, so with and w/o distance should be different
    cirswd = iwd.transform_to(cframe1)
    assert not allclose(cirswd.ra, cirsnod.ra, rtol=1e-8)
    assert not allclose(cirswd.dec, cirsnod.dec, rtol=1e-8)
    # and the distance should transform at least somehow
    assert not allclose(cirswd.distance, iwd.distance, rtol=1e-8)

    # now check that the cirs self-transform works as expected
    cirsnod3 = cirsnod.transform_to(cframe1)  # should be a no-op
    assert_allclose(cirsnod.ra, cirsnod3.ra)
    assert_allclose(cirsnod.dec, cirsnod3.dec)

    cirsnod4 = cirsnod.transform_to(cframe2)  # should be different
    assert not allclose(cirsnod4.ra, cirsnod.ra, rtol=1e-8)
    assert not allclose(cirsnod4.dec, cirsnod.dec, rtol=1e-8)

    cirsnod5 = cirsnod4.transform_to(cframe1)  # should be back to the same
    assert_allclose(cirsnod.ra, cirsnod5.ra)
    assert_allclose(cirsnod.dec, cirsnod5.dec)


usph = golden_spiral_grid(200)
dist = np.linspace(0.5, 1, len(usph)) * u.pc
icrs_coords = [ICRS(usph), ICRS(usph.lon, usph.lat, distance=dist)]
gcrs_frames = [GCRS(), GCRS(obstime=Time('J2005'))]


@pytest.mark.parametrize('icoo', icrs_coords)
def test_icrs_gcrs(icoo):
    """
    Check ICRS<->GCRS for consistency
    """
    gcrscoo = icoo.transform_to(gcrs_frames[0])  # uses the default time
    # first do a round-tripping test
    icoo2 = gcrscoo.transform_to(ICRS())
    assert_allclose(icoo.distance, icoo2.distance)
    assert_allclose(icoo.ra, icoo2.ra)
    assert_allclose(icoo.dec, icoo2.dec)
    assert isinstance(icoo2.data, icoo.data.__class__)

    # now check that a different time yields different answers
    gcrscoo2 = icoo.transform_to(gcrs_frames[1])
    assert not allclose(gcrscoo.ra, gcrscoo2.ra, rtol=1e-8, atol=1e-10*u.deg)
    assert not allclose(gcrscoo.dec, gcrscoo2.dec, rtol=1e-8, atol=1e-10*u.deg)

    # now check that the cirs self-transform works as expected
    gcrscoo3 = gcrscoo.transform_to(gcrs_frames[0])  # should be a no-op
    assert_allclose(gcrscoo.ra, gcrscoo3.ra)
    assert_allclose(gcrscoo.dec, gcrscoo3.dec)

    gcrscoo4 = gcrscoo.transform_to(gcrs_frames[1])  # should be different
    assert not allclose(gcrscoo4.ra, gcrscoo.ra, rtol=1e-8, atol=1e-10*u.deg)
    assert not allclose(gcrscoo4.dec, gcrscoo.dec, rtol=1e-8, atol=1e-10*u.deg)

    gcrscoo5 = gcrscoo4.transform_to(gcrs_frames[0])  # should be back to the same
    assert_allclose(gcrscoo.ra, gcrscoo5.ra, rtol=1e-8, atol=1e-10*u.deg)
    assert_allclose(gcrscoo.dec, gcrscoo5.dec, rtol=1e-8, atol=1e-10*u.deg)

    # also make sure that a GCRS with a different geoloc/geovel gets a different answer
    # roughly a moon-like frame
    gframe3 = GCRS(obsgeoloc=[385000., 0, 0]*u.km, obsgeovel=[1, 0, 0]*u.km/u.s)
    gcrscoo6 = icoo.transform_to(gframe3)  # should be different
    assert not allclose(gcrscoo.ra, gcrscoo6.ra, rtol=1e-8, atol=1e-10*u.deg)
    assert not allclose(gcrscoo.dec, gcrscoo6.dec, rtol=1e-8, atol=1e-10*u.deg)
    icooviag3 = gcrscoo6.transform_to(ICRS())  # and now back to the original
    assert_allclose(icoo.ra, icooviag3.ra)
    assert_allclose(icoo.dec, icooviag3.dec)


@pytest.mark.parametrize('gframe', gcrs_frames)
def test_icrs_gcrs_dist_diff(gframe):
    """
    Check that with and without distance give different ICRS<->GCRS answers
    """
    gcrsnod = icrs_coords[0].transform_to(gframe)
    gcrswd = icrs_coords[1].transform_to(gframe)

    # parallax effects should be included, so with and w/o distance should be different
    assert not allclose(gcrswd.ra, gcrsnod.ra, rtol=1e-8, atol=1e-10*u.deg)
    assert not allclose(gcrswd.dec, gcrsnod.dec, rtol=1e-8, atol=1e-10*u.deg)
    # and the distance should transform at least somehow
    assert not allclose(gcrswd.distance, icrs_coords[1].distance, rtol=1e-8,
                        atol=1e-10*u.pc)


def test_cirs_to_altaz():
    """
    Check the basic CIRS<->AltAz transforms.  More thorough checks implicitly
    happen in `test_iau_fullstack`
    """
    from astropy.coordinates import EarthLocation

    usph = golden_spiral_grid(200)
    dist = np.linspace(0.5, 1, len(usph)) * u.pc
    cirs = CIRS(usph, obstime='J2000')
    crepr = SphericalRepresentation(lon=usph.lon, lat=usph.lat, distance=dist)
    cirscart = CIRS(crepr, obstime=cirs.obstime, representation_type=CartesianRepresentation)

    loc = EarthLocation(lat=0*u.deg, lon=0*u.deg, height=0*u.m)
    altazframe = AltAz(location=loc, obstime=Time('J2005'))

    cirs2 = cirs.transform_to(altazframe).transform_to(cirs)
    cirs3 = cirscart.transform_to(altazframe).transform_to(cirs)

    # check round-tripping
    assert_allclose(cirs.ra, cirs2.ra)
    assert_allclose(cirs.dec, cirs2.dec)
    assert_allclose(cirs.ra, cirs3.ra)
    assert_allclose(cirs.dec, cirs3.dec)


def test_cirs_to_hadec():
    """
    Check the basic CIRS<->HADec transforms.
    """
    from astropy.coordinates import EarthLocation

    usph = golden_spiral_grid(200)
    dist = np.linspace(0.5, 1, len(usph)) * u.pc
    cirs = CIRS(usph, obstime='J2000')
    crepr = SphericalRepresentation(lon=usph.lon, lat=usph.lat, distance=dist)
    cirscart = CIRS(crepr, obstime=cirs.obstime, representation_type=CartesianRepresentation)

    loc = EarthLocation(lat=0*u.deg, lon=0*u.deg, height=0*u.m)
    hadecframe = HADec(location=loc, obstime=Time('J2005'))

    cirs2 = cirs.transform_to(hadecframe).transform_to(cirs)
    cirs3 = cirscart.transform_to(hadecframe).transform_to(cirs)

    # check round-tripping
    assert_allclose(cirs.ra, cirs2.ra)
    assert_allclose(cirs.dec, cirs2.dec)
    assert_allclose(cirs.ra, cirs3.ra)
    assert_allclose(cirs.dec, cirs3.dec)


def test_gcrs_itrs():
    """
    Check basic GCRS<->ITRS transforms for round-tripping.
    """
    usph = golden_spiral_grid(200)
    gcrs = GCRS(usph, obstime='J2000')
    gcrs6 = GCRS(usph, obstime='J2006')

    gcrs2 = gcrs.transform_to(ITRS()).transform_to(gcrs)
    gcrs6_2 = gcrs6.transform_to(ITRS()).transform_to(gcrs)

    assert_allclose(gcrs.ra, gcrs2.ra)
    assert_allclose(gcrs.dec, gcrs2.dec)
    # these should be different:
    assert not allclose(gcrs.ra, gcrs6_2.ra, rtol=1e-8)
    assert not allclose(gcrs.dec, gcrs6_2.dec, rtol=1e-8)

    # also try with the cartesian representation
    gcrsc = gcrs.realize_frame(gcrs.data)
    gcrsc.representation_type = CartesianRepresentation
    gcrsc2 = gcrsc.transform_to(ITRS()).transform_to(gcrsc)
    assert_allclose(gcrsc.spherical.lon, gcrsc2.ra)
    assert_allclose(gcrsc.spherical.lat, gcrsc2.dec)


def test_cirs_itrs():
    """
    Check basic CIRS<->ITRS transforms for round-tripping.
    """
    usph = golden_spiral_grid(200)
    cirs = CIRS(usph, obstime='J2000')
    cirs6 = CIRS(usph, obstime='J2006')

    cirs2 = cirs.transform_to(ITRS()).transform_to(cirs)
    cirs6_2 = cirs6.transform_to(ITRS()).transform_to(cirs)  # different obstime

    # just check round-tripping
    assert_allclose(cirs.ra, cirs2.ra)
    assert_allclose(cirs.dec, cirs2.dec)
    assert not allclose(cirs.ra, cirs6_2.ra)
    assert not allclose(cirs.dec, cirs6_2.dec)


def test_gcrs_cirs():
    """
    Check GCRS<->CIRS transforms for round-tripping.  More complicated than the
    above two because it's multi-hop
    """
    usph = golden_spiral_grid(200)
    gcrs = GCRS(usph, obstime='J2000')
    gcrs6 = GCRS(usph, obstime='J2006')

    gcrs2 = gcrs.transform_to(CIRS()).transform_to(gcrs)
    gcrs6_2 = gcrs6.transform_to(CIRS()).transform_to(gcrs)

    assert_allclose(gcrs.ra, gcrs2.ra)
    assert_allclose(gcrs.dec, gcrs2.dec)
    # these should be different:
    assert not allclose(gcrs.ra, gcrs6_2.ra, rtol=1e-8)
    assert not allclose(gcrs.dec, gcrs6_2.dec, rtol=1e-8)

    # now try explicit intermediate pathways and ensure they're all consistent
    gcrs3 = gcrs.transform_to(ITRS()).transform_to(CIRS()).transform_to(ITRS()).transform_to(gcrs)
    assert_allclose(gcrs.ra, gcrs3.ra)
    assert_allclose(gcrs.dec, gcrs3.dec)

    gcrs4 = gcrs.transform_to(ICRS()).transform_to(CIRS()).transform_to(ICRS()).transform_to(gcrs)
    assert_allclose(gcrs.ra, gcrs4.ra)
    assert_allclose(gcrs.dec, gcrs4.dec)


def test_gcrs_altaz():
    """
    Check GCRS<->AltAz transforms for round-tripping.  Has multiple paths
    """
    from astropy.coordinates import EarthLocation

    usph = golden_spiral_grid(128)
    gcrs = GCRS(usph, obstime='J2000')[None]  # broadcast with times below

    # check array times sure N-d arrays work
    times = Time(np.linspace(2456293.25, 2456657.25, 51) * u.day,
                 format='jd')[:, None]

    loc = EarthLocation(lon=10 * u.deg, lat=80. * u.deg)
    aaframe = AltAz(obstime=times, location=loc)

    aa1 = gcrs.transform_to(aaframe)
    aa2 = gcrs.transform_to(ICRS()).transform_to(CIRS()).transform_to(aaframe)
    aa3 = gcrs.transform_to(ITRS()).transform_to(CIRS()).transform_to(aaframe)

    # make sure they're all consistent
    assert_allclose(aa1.alt, aa2.alt)
    assert_allclose(aa1.az, aa2.az)
    assert_allclose(aa1.alt, aa3.alt)
    assert_allclose(aa1.az, aa3.az)


def test_gcrs_hadec():
    """
    Check GCRS<->HADec transforms for round-tripping.  Has multiple paths
    """
    from astropy.coordinates import EarthLocation

    usph = golden_spiral_grid(128)
    gcrs = GCRS(usph, obstime='J2000')  # broadcast with times below

    # check array times sure N-d arrays work
    times = Time(np.linspace(2456293.25, 2456657.25, 51) * u.day,
                 format='jd')[:, np.newaxis]

    loc = EarthLocation(lon=10 * u.deg, lat=80. * u.deg)
    hdframe = HADec(obstime=times, location=loc)

    hd1 = gcrs.transform_to(hdframe)
    hd2 = gcrs.transform_to(ICRS()).transform_to(CIRS()).transform_to(hdframe)
    hd3 = gcrs.transform_to(ITRS()).transform_to(CIRS()).transform_to(hdframe)

    # make sure they're all consistent
    assert_allclose(hd1.dec, hd2.dec)
    assert_allclose(hd1.ha, hd2.ha)
    assert_allclose(hd1.dec, hd3.dec)
    assert_allclose(hd1.ha, hd3.ha)


def test_precessed_geocentric():
    assert PrecessedGeocentric().equinox.jd == Time('J2000').jd

    gcrs_coo = GCRS(180*u.deg, 2*u.deg, distance=10000*u.km)
    pgeo_coo = gcrs_coo.transform_to(PrecessedGeocentric())
    assert np.abs(gcrs_coo.ra - pgeo_coo.ra) > 10*u.marcsec
    assert np.abs(gcrs_coo.dec - pgeo_coo.dec) > 10*u.marcsec
    assert_allclose(gcrs_coo.distance, pgeo_coo.distance)

    gcrs_roundtrip = pgeo_coo.transform_to(GCRS())
    assert_allclose(gcrs_coo.ra, gcrs_roundtrip.ra)
    assert_allclose(gcrs_coo.dec, gcrs_roundtrip.dec)
    assert_allclose(gcrs_coo.distance, gcrs_roundtrip.distance)

    pgeo_coo2 = gcrs_coo.transform_to(PrecessedGeocentric(equinox='B1850'))
    assert np.abs(gcrs_coo.ra - pgeo_coo2.ra) > 1.5*u.deg
    assert np.abs(gcrs_coo.dec - pgeo_coo2.dec) > 0.5*u.deg
    assert_allclose(gcrs_coo.distance, pgeo_coo2.distance)

    gcrs2_roundtrip = pgeo_coo2.transform_to(GCRS())
    assert_allclose(gcrs_coo.ra, gcrs2_roundtrip.ra)
    assert_allclose(gcrs_coo.dec, gcrs2_roundtrip.dec)
    assert_allclose(gcrs_coo.distance, gcrs2_roundtrip.distance)


def test_precessed_geocentric_different_obstime():
    # Create two PrecessedGeocentric frames with different obstime
    precessedgeo1 = PrecessedGeocentric(obstime='2021-09-07')
    precessedgeo2 = PrecessedGeocentric(obstime='2021-06-07')

    # GCRS->PrecessedGeocentric should give different results for the two frames
    gcrs_coord = GCRS(10*u.deg, 20*u.deg, 3*u.AU, obstime=precessedgeo1.obstime)
    pg_coord1 = gcrs_coord.transform_to(precessedgeo1)
    pg_coord2 = gcrs_coord.transform_to(precessedgeo2)
    assert not pg_coord1.is_equivalent_frame(pg_coord2)
    assert not allclose(pg_coord1.cartesian.xyz, pg_coord2.cartesian.xyz)

    # Looping back to GCRS should return the original coordinate
    loopback1 = pg_coord1.transform_to(gcrs_coord)
    loopback2 = pg_coord2.transform_to(gcrs_coord)
    assert loopback1.is_equivalent_frame(gcrs_coord)
    assert loopback2.is_equivalent_frame(gcrs_coord)
    assert_allclose(loopback1.cartesian.xyz, gcrs_coord.cartesian.xyz)
    assert_allclose(loopback2.cartesian.xyz, gcrs_coord.cartesian.xyz)


# shared by parametrized tests below.  Some use the whole AltAz, others use just obstime
totest_frames = [AltAz(location=EarthLocation(-90*u.deg, 65*u.deg),
                       obstime=Time('J2000')),  # J2000 is often a default so this might work when others don't
                 AltAz(location=EarthLocation(120*u.deg, -35*u.deg),
                       obstime=Time('J2000')),
                 AltAz(location=EarthLocation(-90*u.deg, 65*u.deg),
                       obstime=Time('2014-01-01 00:00:00')),
                 AltAz(location=EarthLocation(-90*u.deg, 65*u.deg),
                       obstime=Time('2014-08-01 08:00:00')),
                 AltAz(location=EarthLocation(120*u.deg, -35*u.deg),
                       obstime=Time('2014-01-01 00:00:00'))
                 ]
MOONDIST = 385000*u.km  # approximate moon semi-major orbit axis of moon
MOONDIST_CART = CartesianRepresentation(3**-0.5*MOONDIST, 3**-0.5*MOONDIST, 3**-0.5*MOONDIST)
EARTHECC = 0.017 + 0.005  # roughly earth orbital eccentricity, but with an added tolerance


@pytest.mark.parametrize('testframe', totest_frames)
def test_gcrs_altaz_sunish(testframe):
    """
    Sanity-check that the sun is at a reasonable distance from any altaz
    """
    sun = get_sun(testframe.obstime)

    assert sun.frame.name == 'gcrs'

    # the .to(u.au) is not necessary, it just makes the asserts on failure more readable
    assert (EARTHECC - 1)*u.au < sun.distance.to(u.au) < (EARTHECC + 1)*u.au

    sunaa = sun.transform_to(testframe)
    assert (EARTHECC - 1)*u.au < sunaa.distance.to(u.au) < (EARTHECC + 1)*u.au


@pytest.mark.parametrize('testframe', totest_frames)
def test_gcrs_altaz_moonish(testframe):
    """
    Sanity-check that an object resembling the moon goes to the right place with
    a GCRS->AltAz transformation
    """
    moon = GCRS(MOONDIST_CART, obstime=testframe.obstime)

    moonaa = moon.transform_to(testframe)

    # now check that the distance change is similar to earth radius
    assert 1000*u.km < np.abs(moonaa.distance - moon.distance).to(u.au) < 7000*u.km

    # now check that it round-trips
    moon2 = moonaa.transform_to(moon)
    assert_allclose(moon.cartesian.xyz, moon2.cartesian.xyz)

    # also should add checks that the alt/az are different for different earth locations


@pytest.mark.parametrize('testframe', totest_frames)
def test_gcrs_altaz_bothroutes(testframe):
    """
    Repeat of both the moonish and sunish tests above to make sure the two
    routes through the coordinate graph are consistent with each other
    """
    sun = get_sun(testframe.obstime)
    sunaa_viaicrs = sun.transform_to(ICRS()).transform_to(testframe)
    sunaa_viaitrs = sun.transform_to(ITRS(obstime=testframe.obstime)).transform_to(testframe)

    moon = GCRS(MOONDIST_CART, obstime=testframe.obstime)
    moonaa_viaicrs = moon.transform_to(ICRS()).transform_to(testframe)
    moonaa_viaitrs = moon.transform_to(ITRS(obstime=testframe.obstime)).transform_to(testframe)

    assert_allclose(sunaa_viaicrs.cartesian.xyz, sunaa_viaitrs.cartesian.xyz)
    assert_allclose(moonaa_viaicrs.cartesian.xyz, moonaa_viaitrs.cartesian.xyz)


@pytest.mark.parametrize('testframe', totest_frames)
def test_cirs_altaz_moonish(testframe):
    """
    Sanity-check that an object resembling the moon goes to the right place with
    a CIRS<->AltAz transformation
    """
    moon = CIRS(MOONDIST_CART, obstime=testframe.obstime)

    moonaa = moon.transform_to(testframe)
    assert 1000*u.km < np.abs(moonaa.distance - moon.distance).to(u.km) < 7000*u.km

    # now check that it round-trips
    moon2 = moonaa.transform_to(moon)
    assert_allclose(moon.cartesian.xyz, moon2.cartesian.xyz)


@pytest.mark.parametrize('testframe', totest_frames)
def test_cirs_altaz_nodist(testframe):
    """
    Check that a UnitSphericalRepresentation coordinate round-trips for the
    CIRS<->AltAz transformation.
    """
    coo0 = CIRS(UnitSphericalRepresentation(10*u.deg, 20*u.deg), obstime=testframe.obstime)

    # check that it round-trips
    coo1 = coo0.transform_to(testframe).transform_to(coo0)
    assert_allclose(coo0.cartesian.xyz, coo1.cartesian.xyz)


@pytest.mark.parametrize('testframe', totest_frames)
def test_cirs_icrs_moonish(testframe):
    """
    check that something like the moon goes to about the right distance from the
    ICRS origin when starting from CIRS
    """
    moonish = CIRS(MOONDIST_CART, obstime=testframe.obstime)
    moonicrs = moonish.transform_to(ICRS())

    assert 0.97*u.au < moonicrs.distance < 1.03*u.au


@pytest.mark.parametrize('testframe', totest_frames)
def test_gcrs_icrs_moonish(testframe):
    """
    check that something like the moon goes to about the right distance from the
    ICRS origin when starting from GCRS
    """
    moonish = GCRS(MOONDIST_CART, obstime=testframe.obstime)
    moonicrs = moonish.transform_to(ICRS())

    assert 0.97*u.au < moonicrs.distance < 1.03*u.au


@pytest.mark.parametrize('testframe', totest_frames)
def test_icrs_gcrscirs_sunish(testframe):
    """
    check that the ICRS barycenter goes to about the right distance from various
    ~geocentric frames (other than testframe)
    """
    # slight offset to avoid divide-by-zero errors
    icrs = ICRS(0*u.deg, 0*u.deg, distance=10*u.km)

    gcrs = icrs.transform_to(GCRS(obstime=testframe.obstime))
    assert (EARTHECC - 1)*u.au < gcrs.distance.to(u.au) < (EARTHECC + 1)*u.au

    cirs = icrs.transform_to(CIRS(obstime=testframe.obstime))
    assert (EARTHECC - 1)*u.au < cirs.distance.to(u.au) < (EARTHECC + 1)*u.au

    itrs = icrs.transform_to(ITRS(obstime=testframe.obstime))
    assert (EARTHECC - 1)*u.au < itrs.spherical.distance.to(u.au) < (EARTHECC + 1)*u.au


@pytest.mark.parametrize('testframe', totest_frames)
def test_icrs_altaz_moonish(testframe):
    """
    Check that something expressed in *ICRS* as being moon-like goes to the
    right AltAz distance
    """
    # we use epv00 instead of get_sun because get_sun includes aberration
    earth_pv_helio, earth_pv_bary = erfa.epv00(*get_jd12(testframe.obstime, 'tdb'))
    earth_icrs_xyz = earth_pv_bary[0]*u.au
    moonoffset = [0, 0, MOONDIST.value]*MOONDIST.unit
    moonish_icrs = ICRS(CartesianRepresentation(earth_icrs_xyz + moonoffset))
    moonaa = moonish_icrs.transform_to(testframe)

    # now check that the distance change is similar to earth radius
    assert 1000*u.km < np.abs(moonaa.distance - MOONDIST).to(u.au) < 7000*u.km


def test_gcrs_self_transform_closeby():
    """
    Tests GCRS self transform for objects which are nearby and thus
    have reasonable parallax.

    Moon positions were originally created using JPL DE432s ephemeris.

    The two lunar positions (one geocentric, one at a defined location)
    are created via a transformation from ICRS to two different GCRS frames.

    We test that the GCRS-GCRS self transform can correctly map one GCRS
    frame onto the other.
    """
    t = Time("2014-12-25T07:00")
    moon_geocentric = SkyCoord(GCRS(318.10579159*u.deg,
                                    -11.65281165*u.deg,
                                    365042.64880308*u.km, obstime=t))

    # this is the location of the Moon as seen from La Palma
    obsgeoloc = [-5592982.59658935, -63054.1948592, 3059763.90102216]*u.m
    obsgeovel = [4.59798494, -407.84677071, 0.]*u.m/u.s
    moon_lapalma = SkyCoord(GCRS(318.7048445*u.deg,
                                 -11.98761996*u.deg,
                                 369722.8231031*u.km,
                                 obstime=t,
                                 obsgeoloc=obsgeoloc,
                                 obsgeovel=obsgeovel))

    transformed = moon_geocentric.transform_to(moon_lapalma.frame)
    delta = transformed.separation_3d(moon_lapalma)
    assert_allclose(delta, 0.0*u.m, atol=1*u.m)


def test_teme_itrf():
    """
    Test case transform from TEME to ITRF.

    Test case derives from example on appendix C of Vallado, Crawford, Hujsak & Kelso (2006).
    See https://celestrak.com/publications/AIAA/2006-6753/AIAA-2006-6753-Rev2.pdf
    """
    v_itrf = CartesianDifferential(-3.225636520, -2.872451450, 5.531924446,
                                   unit=u.km/u.s)
    p_itrf = CartesianRepresentation(-1033.479383, 7901.2952740, 6380.35659580,
                                     unit=u.km, differentials={'s': v_itrf})
    t = Time("2004-04-06T07:51:28.386")

    teme = ITRS(p_itrf, obstime=t).transform_to(TEME(obstime=t))
    v_teme = CartesianDifferential(-4.746131487, 0.785818041, 5.531931288,
                                   unit=u.km/u.s)
    p_teme = CartesianRepresentation(5094.18016210, 6127.64465050, 6380.34453270,
                                     unit=u.km, differentials={'s': v_teme})

    assert_allclose(teme.cartesian.without_differentials().xyz,
                    p_teme.without_differentials().xyz, atol=30*u.cm)

    assert_allclose(teme.cartesian.differentials['s'].d_xyz,
                    p_teme.differentials['s'].d_xyz, atol=1.0*u.cm/u.s)

    # test round trip
    itrf = teme.transform_to(ITRS(obstime=t))
    assert_allclose(
        itrf.cartesian.without_differentials().xyz,
        p_itrf.without_differentials().xyz,
        atol=100*u.cm
    )
    assert_allclose(
        itrf.cartesian.differentials['s'].d_xyz,
        p_itrf.differentials['s'].d_xyz,
        atol=1*u.cm/u.s
    )


def test_precessedgeocentric_loopback():
    from_coo = PrecessedGeocentric(1*u.deg, 2*u.deg, 3*u.AU,
                                   obstime='2001-01-01', equinox='2001-01-01')

    # Change just the obstime
    to_frame = PrecessedGeocentric(obstime='2001-06-30', equinox='2001-01-01')

    explicit_coo = from_coo.transform_to(ICRS()).transform_to(to_frame)
    implicit_coo = from_coo.transform_to(to_frame)

    # Confirm that the explicit transformation changes the coordinate
    assert not allclose(explicit_coo.ra, from_coo.ra, rtol=1e-10)
    assert not allclose(explicit_coo.dec, from_coo.dec, rtol=1e-10)
    assert not allclose(explicit_coo.distance, from_coo.distance, rtol=1e-10)

    # Confirm that the loopback matches the explicit transformation
    assert_allclose(explicit_coo.ra, implicit_coo.ra, rtol=1e-10)
    assert_allclose(explicit_coo.dec, implicit_coo.dec, rtol=1e-10)
    assert_allclose(explicit_coo.distance, implicit_coo.distance, rtol=1e-10)

    # Change just the equinox
    to_frame = PrecessedGeocentric(obstime='2001-01-01', equinox='2001-06-30')

    explicit_coo = from_coo.transform_to(ICRS()).transform_to(to_frame)
    implicit_coo = from_coo.transform_to(to_frame)

    # Confirm that the explicit transformation changes the direction but not the distance
    assert not allclose(explicit_coo.ra, from_coo.ra, rtol=1e-10)
    assert not allclose(explicit_coo.dec, from_coo.dec, rtol=1e-10)
    assert allclose(explicit_coo.distance, from_coo.distance, rtol=1e-10)

    # Confirm that the loopback matches the explicit transformation
    assert_allclose(explicit_coo.ra, implicit_coo.ra, rtol=1e-10)
    assert_allclose(explicit_coo.dec, implicit_coo.dec, rtol=1e-10)
    assert_allclose(explicit_coo.distance, implicit_coo.distance, rtol=1e-10)


def test_teme_loopback():
    from_coo = TEME(1*u.AU, 2*u.AU, 3*u.AU, obstime='2001-01-01')
    to_frame = TEME(obstime='2001-06-30')

    explicit_coo = from_coo.transform_to(ICRS()).transform_to(to_frame)
    implicit_coo = from_coo.transform_to(to_frame)

    # Confirm that the explicit transformation changes the coordinate
    assert not allclose(explicit_coo.cartesian.xyz, from_coo.cartesian.xyz, rtol=1e-10)

    # Confirm that the loopback matches the explicit transformation
    assert_allclose(explicit_coo.cartesian.xyz, implicit_coo.cartesian.xyz, rtol=1e-10)


@pytest.mark.remote_data
def test_earth_orientation_table(monkeypatch):
    """Check that we can set the IERS table used as Earth Reference.

    Use the here and now to be sure we get a difference.
    """
    monkeypatch.setattr('astropy.utils.iers.conf.auto_download', True)
    t = Time.now()
    location = EarthLocation(lat=0*u.deg, lon=0*u.deg)
    altaz = AltAz(location=location, obstime=t)
    sc = SkyCoord(1*u.deg, 2*u.deg)
    # Default: uses IERS_Auto, which will give a prediction.
    # Note: tests run with warnings turned into errors, so it is
    # meaningful if this passes.
    if CI:
        with warnings.catch_warnings():
            # Server occasionally blocks IERS download in CI.
            warnings.filterwarnings('ignore', message=r'.*using local IERS-B.*')
            # This also captures unclosed socket warning that is ignored in setup.cfg
            warnings.filterwarnings('ignore', message=r'.*unclosed.*')
            altaz_auto = sc.transform_to(altaz)
    else:
        altaz_auto = sc.transform_to(altaz)  # No warnings

    with iers.earth_orientation_table.set(iers.IERS_B.open()):
        with pytest.warns(AstropyWarning, match='after IERS data'):
            altaz_b = sc.transform_to(altaz)

    sep_b_auto = altaz_b.separation(altaz_auto)
    assert_allclose(sep_b_auto, 0.0*u.deg, atol=1*u.arcsec)
    assert sep_b_auto > 10*u.microarcsecond

    # Check we returned to regular IERS system.
    altaz_auto2 = sc.transform_to(altaz)
    assert altaz_auto2.separation(altaz_auto) == 0.


@pytest.mark.remote_data
@pytest.mark.skipif(not HAS_JPLEPHEM, reason='requires jplephem')
def test_ephemerides():
    """
    We test that using different ephemerides gives very similar results
    for transformations
    """
    t = Time("2014-12-25T07:00")
    moon = SkyCoord(GCRS(318.10579159*u.deg,
                         -11.65281165*u.deg,
                         365042.64880308*u.km, obstime=t))

    icrs_frame = ICRS()
    hcrs_frame = HCRS(obstime=t)
    ecl_frame = HeliocentricMeanEcliptic(equinox=t)
    cirs_frame = CIRS(obstime=t)

    moon_icrs_builtin = moon.transform_to(icrs_frame)
    moon_hcrs_builtin = moon.transform_to(hcrs_frame)
    moon_helioecl_builtin = moon.transform_to(ecl_frame)
    moon_cirs_builtin = moon.transform_to(cirs_frame)

    with solar_system_ephemeris.set('jpl'):
        moon_icrs_jpl = moon.transform_to(icrs_frame)
        moon_hcrs_jpl = moon.transform_to(hcrs_frame)
        moon_helioecl_jpl = moon.transform_to(ecl_frame)
        moon_cirs_jpl = moon.transform_to(cirs_frame)

    # most transformations should differ by an amount which is
    # non-zero but of order milliarcsecs
    sep_icrs = moon_icrs_builtin.separation(moon_icrs_jpl)
    sep_hcrs = moon_hcrs_builtin.separation(moon_hcrs_jpl)
    sep_helioecl = moon_helioecl_builtin.separation(moon_helioecl_jpl)
    sep_cirs = moon_cirs_builtin.separation(moon_cirs_jpl)

    assert_allclose([sep_icrs, sep_hcrs, sep_helioecl], 0.0*u.deg, atol=10*u.mas)
    assert all(sep > 10*u.microarcsecond for sep in (sep_icrs, sep_hcrs, sep_helioecl))

    # CIRS should be the same
    assert_allclose(sep_cirs, 0.0*u.deg, atol=1*u.microarcsecond)


def test_tete_transforms():
    """
    We test the TETE transforms for proper behaviour here.

    The TETE transforms are tested for accuracy against JPL Horizons in
    test_solar_system.py. Here we are looking to check for consistency and
    errors in the self transform.
    """
    loc = EarthLocation.from_geodetic("-22°57'35.1", "-67°47'14.1", 5186*u.m)
    time = Time('2020-04-06T00:00')
    p, v = loc.get_gcrs_posvel(time)

    gcrs_frame = GCRS(obstime=time, obsgeoloc=p, obsgeovel=v)
    moon = SkyCoord(169.24113968*u.deg, 10.86086666*u.deg, 358549.25381755*u.km, frame=gcrs_frame)

    tete_frame = TETE(obstime=time, location=loc)
    # need to set obsgeoloc/vel explicitly or skycoord behaviour over-writes
    tete_geo = TETE(obstime=time, location=EarthLocation(*([0, 0, 0]*u.km)))

    # test self-transform by comparing to GCRS-TETE-ITRS-TETE route
    tete_coo1 = moon.transform_to(tete_frame)
    tete_coo2 = moon.transform_to(tete_geo)
    assert_allclose(tete_coo1.separation_3d(tete_coo2), 0*u.mm, atol=1*u.mm)

    # test TETE-ITRS transform by comparing GCRS-CIRS-ITRS to GCRS-TETE-ITRS
    itrs1 = moon.transform_to(CIRS()).transform_to(ITRS())
    itrs2 = moon.transform_to(TETE()).transform_to(ITRS())
    assert_allclose(itrs1.separation_3d(itrs2), 0*u.mm, atol=1*u.mm)

    # test round trip GCRS->TETE->GCRS
    new_moon = moon.transform_to(TETE()).transform_to(moon)
    assert_allclose(new_moon.separation_3d(moon), 0*u.mm, atol=1*u.mm)

    # test round trip via ITRS
    tete_rt = tete_coo1.transform_to(ITRS(obstime=time)).transform_to(tete_coo1)
    assert_allclose(tete_rt.separation_3d(tete_coo1), 0*u.mm, atol=1*u.mm)

    # ensure deprecated routine remains consistent
    # make sure test raises warning!
    with pytest.warns(AstropyDeprecationWarning, match='The use of'):
        tete_alt = _apparent_position_in_true_coordinates(moon)
    assert_allclose(tete_coo1.separation_3d(tete_alt), 0*u.mm, atol=100*u.mm)


def test_straight_overhead():
    """
    With a precise CIRS<->AltAz transformation this should give Alt=90 exactly

    If the CIRS self-transform breaks it won't, due to improper treatment of aberration
    """
    t = Time('J2010')
    obj = EarthLocation(-1*u.deg, 52*u.deg, height=10.*u.km)
    home = EarthLocation(-1*u.deg, 52*u.deg, height=0.*u.km)

    # An object that appears straight overhead - FOR A GEOCENTRIC OBSERVER.
    # Note, this won't be overhead for a topocentric observer because of
    # aberration.
    cirs_geo = obj.get_itrs(t).transform_to(CIRS(obstime=t))

    # now get the Geocentric CIRS position of observatory
    obsrepr = home.get_itrs(t).transform_to(CIRS(obstime=t)).cartesian

    # topocentric CIRS position of a straight overhead object
    cirs_repr = cirs_geo.cartesian - obsrepr

    # create a CIRS object that appears straight overhead for a TOPOCENTRIC OBSERVER
    topocentric_cirs_frame = CIRS(obstime=t, location=home)
    cirs_topo = topocentric_cirs_frame.realize_frame(cirs_repr)

    # Check AltAz (though Azimuth can be anything so is not tested).
    aa = cirs_topo.transform_to(AltAz(obstime=t, location=home))
    assert_allclose(aa.alt, 90*u.deg, atol=1*u.uas, rtol=0)

    # Check HADec.
    hd = cirs_topo.transform_to(HADec(obstime=t, location=home))
    assert_allclose(hd.ha, 0*u.hourangle, atol=1*u.uas, rtol=0)
    assert_allclose(hd.dec, 52*u.deg, atol=1*u.uas, rtol=0)


def jplephem_ge(minversion):
    """Check if jplephem is installed and has version >= minversion."""
    # This is a separate routine since somehow with pyinstaller the stanza
    # not HAS_JPLEPHEM or metadata.version('jplephem') < '2.15'
    # leads to a module not found error.
    try:
        return HAS_JPLEPHEM and metadata.version('jplephem') >= minversion
    except Exception:
        return False


@pytest.mark.remote_data
@pytest.mark.skipif(not jplephem_ge('2.15'), reason='requires jplephem >= 2.15')
def test_aa_hd_high_precision():
    """These tests are provided by @mkbrewer - see issue #10356.

    The code that produces them agrees very well (<0.5 mas) with SkyField once Polar motion
    is turned off, but SkyField does not include polar motion, so a comparison to Skyfield
    or JPL Horizons will be ~1" off.

    The absence of polar motion within Skyfield and the disagreement between Skyfield and Horizons
    make high precision comparisons to those codes difficult.

    Updated 2020-11-29, after the comparison between codes became even better,
    down to 100 nas.

    NOTE: the agreement reflects consistency in approach between two codes,
    not necessarily absolute precision.  If this test starts failing, the
    tolerance can and should be weakened *if* it is clear that the change is
    due to an improvement (e.g., a new IAU precession model).

    """
    lat = -22.959748*u.deg
    lon = -67.787260*u.deg
    elev = 5186*u.m
    loc = EarthLocation.from_geodetic(lon, lat, elev)
    # Note: at this level of precision for the comparison, we have to include
    # the location in the time, as it influences the transformation to TDB.
    t = Time('2017-04-06T00:00:00.0', location=loc)
    with solar_system_ephemeris.set('de430'):
        moon = get_body('moon', t, loc)
        moon_aa = moon.transform_to(AltAz(obstime=t, location=loc))
        moon_hd = moon.transform_to(HADec(obstime=t, location=loc))

    # Numbers from
    # https://github.com/astropy/astropy/pull/11073#issuecomment-735486271
    # updated in https://github.com/astropy/astropy/issues/11683
    TARGET_AZ, TARGET_EL = 15.032673509956*u.deg, 50.303110133923*u.deg
    TARGET_DISTANCE = 376252883.247239*u.m
    assert_allclose(moon_aa.az, TARGET_AZ, atol=0.1*u.uas, rtol=0)
    assert_allclose(moon_aa.alt, TARGET_EL, atol=0.1*u.uas, rtol=0)
    assert_allclose(moon_aa.distance, TARGET_DISTANCE, atol=0.1*u.mm, rtol=0)
    ha, dec = erfa.ae2hd(moon_aa.az.to_value(u.radian), moon_aa.alt.to_value(u.radian),
                         lat.to_value(u.radian))
    ha = u.Quantity(ha, u.radian, copy=False)
    dec = u.Quantity(dec, u.radian, copy=False)
    assert_allclose(moon_hd.ha, ha, atol=0.1*u.uas, rtol=0)
    assert_allclose(moon_hd.dec, dec, atol=0.1*u.uas, rtol=0)


def test_aa_high_precision_nodata():
    """
    These tests are designed to ensure high precision alt-az transforms.

    They are a slight fudge since the target values come from astropy itself. They are generated
    with a version of the code that passes the tests above, but for the internal solar system
    ephemerides to avoid the use of remote data.
    """
    # Last updated when switching to erfa 2.0.0 and its moon98 function.
    TARGET_AZ, TARGET_EL = 15.03231495*u.deg, 50.3027193*u.deg
    lat = -22.959748*u.deg
    lon = -67.787260*u.deg
    elev = 5186*u.m
    loc = EarthLocation.from_geodetic(lon, lat, elev)
    t = Time('2017-04-06T00:00:00.0')

    moon = get_body('moon', t, loc)
    moon_aa = moon.transform_to(AltAz(obstime=t, location=loc))
    assert_allclose(moon_aa.az - TARGET_AZ, 0*u.mas, atol=0.5*u.mas)
    assert_allclose(moon_aa.alt - TARGET_EL, 0*u.mas, atol=0.5*u.mas)


class TestGetLocationGCRS:
    # TETE and CIRS use get_location_gcrs to get obsgeoloc and obsgeovel
    # with knowledge of some of the matrices. Check that this is consistent
    # with a direct transformation.
    def setup_class(cls):
        cls.loc = loc = EarthLocation.from_geodetic(
            np.linspace(0, 360, 6)*u.deg, np.linspace(-90, 90, 6)*u.deg, 100*u.m)
        cls.obstime = obstime = Time(np.linspace(2000, 2010, 6), format='jyear')
        # Get comparison via a full transformation.  We do not use any methods
        # of EarthLocation, since those depend on the fast transform.
        loc_itrs = ITRS(loc.x, loc.y, loc.z, obstime=obstime)
        zeros = np.broadcast_to(0. * (u.km / u.s), (3,) + loc_itrs.shape, subok=True)
        loc_itrs.data.differentials['s'] = CartesianDifferential(zeros)
        loc_gcrs_cart = loc_itrs.transform_to(GCRS(obstime=obstime)).cartesian
        cls.obsgeoloc = loc_gcrs_cart.without_differentials()
        cls.obsgeovel = loc_gcrs_cart.differentials['s'].to_cartesian()

    def check_obsgeo(self, obsgeoloc, obsgeovel):
        assert_allclose(obsgeoloc.xyz, self.obsgeoloc.xyz, atol=.1*u.um, rtol=0.)
        assert_allclose(obsgeovel.xyz, self.obsgeovel.xyz, atol=.1*u.mm/u.s, rtol=0.)

    def test_get_gcrs_posvel(self):
        # Really just a sanity check
        self.check_obsgeo(*self.loc.get_gcrs_posvel(self.obstime))

    def test_tete_quick(self):
        # Following copied from intermediate_rotation_transforms.gcrs_to_tete
        rbpn = erfa.pnm06a(*get_jd12(self.obstime, 'tt'))
        loc_gcrs_frame = get_location_gcrs(self.loc, self.obstime,
                                           tete_to_itrs_mat(self.obstime, rbpn=rbpn),
                                           rbpn)
        self.check_obsgeo(loc_gcrs_frame.obsgeoloc, loc_gcrs_frame.obsgeovel)

    def test_cirs_quick(self):
        cirs_frame = CIRS(location=self.loc, obstime=self.obstime)
        # Following copied from intermediate_rotation_transforms.gcrs_to_cirs
        pmat = gcrs_to_cirs_mat(cirs_frame.obstime)
        loc_gcrs_frame = get_location_gcrs(self.loc, self.obstime,
                                           cirs_to_itrs_mat(cirs_frame.obstime), pmat)
        self.check_obsgeo(loc_gcrs_frame.obsgeoloc, loc_gcrs_frame.obsgeovel)
