"""Test suite for SGP4."""

try:
    from unittest2 import TestCase, main
except:
    from unittest import TestCase, main

import datetime as dt
import re
import os
import sys
from doctest import DocTestSuite, ELLIPSIS
from math import pi, isnan
from pkgutil import get_data

try:
    from io import StringIO
except ImportError:
    from StringIO import StringIO

import numpy as np

from sgp4.api import WGS72OLD, WGS72, WGS84, Satrec, jday
from sgp4.earth_gravity import wgs72
from sgp4.ext import invjday, newtonnu, rv2coe
from sgp4.functions import days2mdhms, _day_of_year_to_month_day
from sgp4.propagation import sgp4, sgp4init
from sgp4 import api, conveniences, io, omm
from sgp4.exporter import export_omm, export_tle
import sgp4.model as model

_testcase = TestCase('setUp')
_testcase.maxDiff = 9999
assertEqual = _testcase.assertEqual
assertAlmostEqual = _testcase.assertAlmostEqual
assertRaises = _testcase.assertRaises
assertRaisesRegex = getattr(_testcase, 'assertRaisesRegex',
                            _testcase.assertRaisesRegexp)

error = 2e-7
rad = 180.0 / pi
LINE1 = '1 00005U 58002B   00179.78495062  .00000023  00000-0  28098-4 0  4753'
LINE2 = '2 00005  34.2682 348.7242 1859667 331.7664  19.3264 10.82419157413667'
BAD2  = '2 00007  34.2682 348.7242 1859667 331.7664  19.3264 10.82419157413669'
VANGUARD_ATTRS = {
    # Identity
    'satnum': 5,
    'satnum_str': '00005',
    'classification': 'U',
    'operationmode': 'i',
    # Time
    'epochyr': 0,
    'epochdays': 179.78495062,
    'jdsatepoch': 2451722.5,
    'jdsatepochF': 0.78495062,
    # Orbit
    'bstar': 2.8098e-05,
    'ndot': 6.96919666594958e-13,
    'nddot': 0.0,
    'ecco': 0.1859667,
    'argpo': 5.790416027488515,
    'inclo': 0.5980929187319208,
    'mo': 0.3373093125574321,
    'no_kozai': 0.04722944544077857,
    'nodeo': 6.08638547138321,
}
VANGUARD_EPOCH = 18441.78495062

# Handle deprecated assertRaisesRegexp, but allow its use Python 2.6 and 2.7
if sys.version_info[:2] == (2, 7) or sys.version_info[:2] == (2, 6):
    TestCase.assertRaisesRegex = TestCase.assertRaisesRegexp

# ------------------------------------------------------------------------
#                           Core Attributes
#

def test_satrec_built_with_twoline2rv():
    sat = Satrec.twoline2rv(LINE1, LINE2)
    verify_vanguard_1(sat)

def test_legacy_built_with_twoline2rv():
    sat = io.twoline2rv(LINE1, LINE2, wgs72)
    verify_vanguard_1(sat, legacy=True)

def test_satrec_initialized_with_sgp4init():
    sat = Satrec()
    sat.sgp4init(
        WGS72,
        'i',
        VANGUARD_ATTRS['satnum'],
        VANGUARD_EPOCH,
        *sgp4init_args(VANGUARD_ATTRS)
    )
    verify_vanguard_1(sat)

def test_satrec_initialized_with_sgp4init_in_afspc_mode():
    sat = Satrec()
    sat.sgp4init(
        WGS72,
        'a',
        VANGUARD_ATTRS['satnum'],
        VANGUARD_EPOCH,
        *sgp4init_args(VANGUARD_ATTRS)
    )
    assertEqual(sat.operationmode, 'a')

def test_legacy_initialized_with_sgp4init():
    sat = model.Satellite()
    sgp4init(
        wgs72, 'i', VANGUARD_ATTRS['satnum'], VANGUARD_EPOCH,
        *sgp4init_args(VANGUARD_ATTRS) + (sat,)
    )
    verify_vanguard_1(sat, legacy=True)

# ------------------------------------------------------------------------
#                            Test array API

def test_whether_array_logic_writes_nan_values_to_correct_row():
    # https://github.com/brandon-rhodes/python-sgp4/issues/87
    l1 = "1 44160U 19006AX  20162.79712247 +.00816806 +19088-3 +34711-2 0  9997"
    l2 = "2 44160 095.2472 272.0808 0216413 032.6694 328.7739 15.58006382062511"
    sat = Satrec.twoline2rv(l1, l2)
    jd0 = np.array([2459054.5, 2459055.5])
    jd1 = np.array([0.79712247, 0.79712247])
    e, r, v = sat.sgp4_array(jd0, jd1)
    assert list(e) == [6, 1]
    assert np.isnan(r).tolist() == [[False, False, False], [True, True, True]]
    assert np.isnan(v).tolist() == [[False, False, False], [True, True, True]]

# ------------------------------------------------------------------------
#                 Other Officially Supported Routines
#

def test_days2mdhms():
    # See https://github.com/brandon-rhodes/python-sgp4/issues/64
    tup = days2mdhms(2020, 133.35625)
    assertEqual(tup, (5, 12, 8, 33, 0.0))

def test_jday2():
    jd, fr = jday(2019, 10, 9, 16, 57, 15)
    assertEqual(jd, 2458765.5)
    assertAlmostEqual(fr, 0.7064236111111111)

def test_jday_datetime():
    # define local time
    # UTC equivalent: 2011-11-03 20:05:23+00:00

    class UTC_plus_4(dt.tzinfo):
        'UTC'
        offset = dt.timedelta(hours=4)
        def utcoffset(self, datetime):
            return self.offset
        def tzname(self, datetime):
            return 'UTC plus 4'
        def dst(self, datetime):
            return self.offset

    datetime_local = dt.datetime(2011, 11, 4, 0, 5, 23, 0, UTC_plus_4())
    jd, fr = conveniences.jday_datetime(datetime_local)

    # jd of this date is 2455868.5 + 0.8370717592592593
    assertEqual(jd, 2455868.5)
    assertAlmostEqual(fr, 0.8370717592592593)

def test_sat_epoch_datetime():
    sat = Satrec.twoline2rv(LINE1, LINE2)
    datetime = conveniences.sat_epoch_datetime(sat)
    zone = conveniences.UTC
    assertEqual(datetime, dt.datetime(2000, 6, 27, 18, 50, 19, 733568, zone))

def test_good_tle_checksum():
    for line in LINE1, LINE2:
        checksum = int(line[-1])
        assertEqual(io.compute_checksum(line), checksum)
        assertEqual(io.fix_checksum(line[:68]), line)
        io.verify_checksum(line)

def test_bad_tle_checksum():
    checksum = LINE1[-1]
    assertEqual(checksum, '3')
    bad = LINE1[:68] + '7'
    assertRaises(ValueError, io.verify_checksum, bad)
    assertEqual(io.fix_checksum(bad), LINE1)

def test_tle_export():
    """Check `export_tle()` round-trip using all the TLEs in the test file.

    This iterates through the satellites in "SGP4-VER.TLE",
    generates `Satrec` objects and exports the TLEs.  These exported
    TLEs are then compared to the original TLE, closing the loop (or
    the round-trip).

    """
    data = get_data(__name__, 'SGP4-VER.TLE')
    tle_lines = iter(data.decode('ascii').splitlines())

    # Skip these lines, known errors
    # Resulting TLEs are equivalent (same values in the Satrec object), but they are not the same
    # 25954: BSTAR = 0 results in a negative exp, not positive
    # 29141: BSTAR = 0.13519 results in a negative exp, not positive
    # 33333: Checksum error as expected on both lines
    # 33334: Checksum error as expected on line 1
    # 33335: Checksum error as expected on line 1
    expected_errs_line1 = set([25954, 29141, 33333, 33334, 33335])
    expected_errs_line2 = set([33333, 33335])

    # Non-standard: omits the ephemeris type integer.
    expected_errs_line1.add(11801)

    for line1 in tle_lines:

        if not line1.startswith('1'):
            continue

        line2 = next(tle_lines)

        # trim lines to normal TLE string size
        line1 = line1[:69]
        line2 = line2[:69]
        satrec = Satrec.twoline2rv(line1, line2)
        satrec_old = io.twoline2rv(line1, line2, wgs72)

        # Generate TLE from satrec
        actual_line1, actual_line2 = export_tle(satrec)
        actual_line1_old, actual_line2_old = export_tle(satrec_old)

        if satrec.satnum not in expected_errs_line1:
            assertEqual(actual_line1, line1)
            assertEqual(actual_line1_old, line1)
        if satrec.satnum not in expected_errs_line2:
            assertEqual(actual_line2, line2)
            assertEqual(actual_line2_old, line2)

def test_export_tle_raises_error_for_out_of_range_angles():
    # See https://github.com/brandon-rhodes/python-sgp4/issues/70
    for angle in 'inclo', 'nodeo', 'argpo', 'mo':
        sat = Satrec()
        wrong_vanguard_attrs = VANGUARD_ATTRS.copy()
        wrong_vanguard_attrs[angle] = -1.0
        sat.sgp4init(
            WGS84, 'i', wrong_vanguard_attrs['satnum'], VANGUARD_EPOCH,
            *sgp4init_args(wrong_vanguard_attrs)
        )
        assertRaises(ValueError, export_tle, sat)

def test_tle_import_export_round_trips():
    for line1, line2 in [(
        '1 44542U 19061A   21180.78220369 -.00000015  00000-0 -66561+1 0  9997',
        '2 44542  54.7025 244.1098 0007981 318.8601 283.5781  1.86231125 12011',
    )]:
        sat = Satrec.twoline2rv(line1, line2)
        outline1, outline2 = export_tle(sat)
        assertEqual(line1, outline1)
        assertEqual(line2, outline2)

def test_all_three_gravity_models_with_twoline2rv():
    # The numbers below are those produced by Vallado's C++ code.
    # (Why does the Python version not produce the same values to
    # high accuracy, instead of agreeing to only 4 places?)

    assert_wgs72old(Satrec.twoline2rv(LINE1, LINE2, WGS72OLD))
    assert_wgs72(Satrec.twoline2rv(LINE1, LINE2, WGS72))
    assert_wgs84(Satrec.twoline2rv(LINE1, LINE2, WGS84))

    # Not specifying a gravity model should select WGS72.

    assert_wgs72(Satrec.twoline2rv(LINE1, LINE2))

def test_all_three_gravity_models_with_sgp4init():
    # Gravity models specified with sgp4init() should also change the
    # positions generated.

    sat = Satrec()
    args = sgp4init_args(VANGUARD_ATTRS)

    sat.sgp4init(WGS72OLD, 'i', VANGUARD_ATTRS['satnum'], VANGUARD_EPOCH, *args)
    assert_wgs72old(sat)

    sat.sgp4init(WGS72, 'i', VANGUARD_ATTRS['satnum'], VANGUARD_EPOCH, *args)
    assert_wgs72(sat)

    sat.sgp4init(WGS84, 'i', VANGUARD_ATTRS['satnum'], VANGUARD_EPOCH, *args)
    assert_wgs84(sat)

GRAVITY_DIGITS = (
    # Why don't Python and C agree more closely?
    4 if not api.accelerated

    # Otherwise, try 10 digits.  Note that at least 6 digits past the
    # decimal point are necessary to let the test distinguish between
    # WSG72OLD and WGS72.  See:
    # https://github.com/conda-forge/sgp4-feedstock/pull/19
    # https://github.com/brandon-rhodes/python-sgp4/issues/69
    else 10
)

def assert_wgs72old(sat):
    e, r, v = sat.sgp4_tsince(309.67110720001529)
    assertAlmostEqual(r[0], -3754.251473242793, GRAVITY_DIGITS)
    assertAlmostEqual(r[1], 7876.346815095482, GRAVITY_DIGITS)
    assertAlmostEqual(r[2], 4719.220855042922, GRAVITY_DIGITS)

def assert_wgs72(sat):
    e, r, v = sat.sgp4_tsince(309.67110720001529)
    assertAlmostEqual(r[0], -3754.2514743216166, GRAVITY_DIGITS)
    assertAlmostEqual(r[1], 7876.346817439062, GRAVITY_DIGITS)
    assertAlmostEqual(r[2], 4719.220856478582, GRAVITY_DIGITS)

def assert_wgs84(sat):
    e, r, v = sat.sgp4_tsince(309.67110720001529)
    assertAlmostEqual(r[0], -3754.2437675772426, GRAVITY_DIGITS)
    assertAlmostEqual(r[1], 7876.3549956188945, GRAVITY_DIGITS)
    assertAlmostEqual(r[2], 4719.227897029576, GRAVITY_DIGITS)

# ------------------------------------------------------------------------
#                            Special Cases
#

def test_satnum_leading_spaces():
    # https://github.com/brandon-rhodes/python-sgp4/issues/81
    # https://github.com/brandon-rhodes/python-sgp4/issues/90
    l1 = '1  4859U 21001A   21007.63955392  .00000000  00000+0  00000+0 0  9990'
    l2 = '2  4859 000.0000 000.0000 0000000 000.0000 000.0000 01.00000000    09'
    sat = Satrec.twoline2rv(l1, l2)
    assertEqual(sat.satnum, 4859)
    assertEqual(sat.classification, 'U')
    assertEqual(sat.intldesg, '21001A')

def test_satnum_alpha5_encoding():
    def make_sat(satnum_string):
        return Satrec.twoline2rv(LINE1.replace('00005', satnum_string),
                                 LINE2.replace('00005', satnum_string))

    # Test cases from https://www.space-track.org/documentation#tle-alpha5
    cases = [(100000, 'A0000'),
             (148493, 'E8493'),
             (182931, 'J2931'),
             (234018, 'P4018'),
             (301928, 'W1928'),
             (339999, 'Z9999')]

    for satnum, satnum_string in cases:
        sat = make_sat(satnum_string)
        assertEqual(sat.satnum, satnum)
        assertEqual(sat.satnum_str, satnum_string)

    args = sgp4init_args(VANGUARD_ATTRS)
    for satnum, satnum_string in cases:
        sat.sgp4init(WGS72, 'i', satnum, VANGUARD_EPOCH, *args)
        assertEqual(sat.satnum, satnum)

def test_satnum_that_is_too_large():
    sat = Satrec()
    with assertRaisesRegex(ValueError, 'cannot exceed 339999'):
        sat.sgp4init(
            WGS72,
            'i',
            340000,
            VANGUARD_EPOCH,
            *sgp4init_args(VANGUARD_ATTRS)
        )

def test_intldesg_with_6_characters():
    sat = Satrec.twoline2rv(LINE1, LINE2)
    assertEqual(sat.intldesg, '58002B')

def test_intldesg_with_7_characters():
    sat = Satrec.twoline2rv(
        '1 39444U 13066AE  20110.89708219  .00000236  00000-0'
        '  35029-4 0  9992',
        '2 39444  97.5597 114.3769 0059573 102.0933 258.6965 '
        '14.82098949344697',
    )
    assertEqual(sat.intldesg, '13066AE')

def test_1990s_satrec_initialized_with_sgp4init():
    sat = Satrec()
    sat.sgp4init(
        WGS72,
        'i',
        VANGUARD_ATTRS['satnum'],
        VANGUARD_EPOCH - 365.0,  # change year 2000 to 1999
        *sgp4init_args(VANGUARD_ATTRS)
    )
    assertEqual(sat.epochyr, 99)

def test_setters():
    sat = Satrec()

    sat.classification = 'S'
    assert sat.classification == 'S'

    sat.intldesg = 'abcdefg'
    assert sat.intldesg == 'abcdefg'

    sat.ephtype = 23
    assert sat.ephtype == 23

    sat.elnum = 123
    assert sat.elnum == 123

    sat.revnum = 1234
    assert sat.revnum == 1234

    sat.satnum_str = 'abcde'
    assert sat.satnum_str == 'abcde'

def test_hyperbolic_orbit():
    # Exercise the newtonnu() code path with asinh() to see whether
    # we can replace it with the one from Python's math module.

    e0, m = newtonnu(1.0, 2.9)  # parabolic
    assertAlmostEqual(e0, 8.238092752965605, places=12)
    assertAlmostEqual(m, 194.60069989482898, places=12)

    e0, m = newtonnu(1.1, 2.7)   # hyperbolic
    assertAlmostEqual(e0, 4.262200676156417, places=12)
    assertAlmostEqual(m, 34.76134082028372, places=12)

def test_correct_epochyr():
    # Make sure that the non-standard four-digit epochyr I switched
    # to in the Python version of SGP4 is reverted back to the
    # official behavior when that code is used behind Satrec.
    sat = Satrec.twoline2rv(LINE1, LINE2)
    assertEqual(sat.epochyr, 0)

def test_legacy_epochyr():
    # Apparently I saw fit to change the meaning of this attribute
    # in the Python version of SGP4.
    sat = io.twoline2rv(LINE1, LINE2, wgs72)
    assertEqual(sat.epochyr, 2000)

def test_support_for_old_no_attribute():
    s = io.twoline2rv(LINE1, LINE2, wgs72)
    assert s.no == s.no_kozai

def test_months_and_days():
    # Make sure our hand-written months-and-days routine is perfect.

    month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]

    day_of_year = 1
    for month, length in enumerate(month_lengths, 1):
        for day in range(1, length + 1):
            tup = _day_of_year_to_month_day(day_of_year, False)
            assertEqual((month, day), tup)
            day_of_year += 1

    month_lengths[1] = 29  # February, during a leap year
    day_of_year = 1
    for month, length in enumerate(month_lengths, 1):
        for day in range(1, length + 1):
            tup = _day_of_year_to_month_day(day_of_year, True)
            assertEqual((month, day), tup)
            day_of_year += 1

def test_december_32():
    # ISS [Orbit 606], whose date is 2019 plus 366.82137887 days.
    # The core SGP4 routines handled this fine, but my hamfisted
    # attempt to provide a Python datetime for "convenience" ran
    # into an overflow.
    a = '1 25544U 98067A   19366.82137887  .00016717  00000-0  10270-3 0  9129'
    b = '2 25544  51.6392  96.6358 0005156  88.7140 271.4601 15.49497216  6061'
    correct_epoch = dt.datetime(2020, 1, 1, 19, 42, 47, 134368)

    # Legacy API.
    sat = io.twoline2rv(a, b, wgs72)
    assertEqual(sat.epoch, correct_epoch)

    correct_epoch = correct_epoch.replace(tzinfo=conveniences.UTC)

    # Modern API.
    sat = Satrec.twoline2rv(a, b)
    assertEqual(conveniences.sat_epoch_datetime(sat), correct_epoch)

def test_non_ascii_first_line():
    if sys.version_info < (3,):
        return
    with assertRaisesRegex(ValueError, re.escape("""your TLE lines are broken because they contain non-ASCII characters:

1 00005U 58002B   00179.78495062  .00000023\\xa0 00000-0  28098-4 0  4753
2 00005  34.2682 348.7242 1859667 331.7664  19.3264 10.82419157413667""")):
        io.twoline2rv(LINE1.replace('23 ', '23\xa0'), LINE2, wgs72)

def test_non_ascii_second_line():
    if sys.version_info < (3,):
        return
    with assertRaisesRegex(ValueError, re.escape("""your TLE lines are broken because they contain non-ASCII characters:

1 00005U 58002B   00179.78495062  .00000023  00000-0  28098-4 0  4753
2 00005 \\xa034.2682\\xa0348.7242 1859667 331.7664  19.3264 10.82419157413667\
""")):
        io.twoline2rv(LINE1, LINE2.replace(' 34', '\xa034'), wgs72)

def test_bad_first_line():
    with assertRaisesRegex(ValueError, re.escape("""TLE format error

The Two-Line Element (TLE) format was designed for punch cards, and so
is very strict about the position of every period, space, and digit.
Your line does not quite match.  Here is the official format for line 1
with an N where each digit should go, followed by the line you provided:

1 NNNNNC NNNNNAAA NNNNN.NNNNNNNN +.NNNNNNNN +NNNNN-N +NNNNN-N N NNNNN
1 00005U 58002B   00179.78495062  .000000234 00000-0  28098-4 0  4753""")):
        io.twoline2rv(LINE1.replace('23 ', '234'), LINE2, wgs72)

def test_bad_second_line():
    with assertRaisesRegex(ValueError, re.escape("""TLE format error

The Two-Line Element (TLE) format was designed for punch cards, and so
is very strict about the position of every period, space, and digit.
Your line does not quite match.  Here is the official format for line 2
with an N where each digit should go, followed by the line you provided:

2 NNNNN NNN.NNNN NNN.NNNN NNNNNNN NNN.NNNN NNN.NNNN NN.NNNNNNNNNNNNNN
2 00005 34 .268234 8.7242 1859667 331.7664  19.3264 10.82419157413667""")):
        io.twoline2rv(LINE1, LINE2.replace(' 34', '34 '), wgs72)

def test_mismatched_lines():
    msg = "Object numbers in lines 1 and 2 do not match"
    with assertRaisesRegex(ValueError, re.escape(msg)):
        io.twoline2rv(LINE1, BAD2, wgs72)

# ------------------------------------------------------------------------
#                           Helper routines
#

def verify_vanguard_1(sat, legacy=False):
    attrs = VANGUARD_ATTRS

    if legacy:
        attrs = attrs.copy()
        del attrs['epochyr']
        del attrs['epochdays']
        del attrs['jdsatepoch']
        del attrs['jdsatepochF']

    for name, value in attrs.items():
        try:
            assertEqual(getattr(sat, name), value)
        except AssertionError as e:
            message, = e.args
            e.args = ('for attribute %s, %s' % (name, message),)
            raise e

def sgp4init_args(d):
    """Given a dict of orbital parameters, return them in sgp4init order."""
    return (d['bstar'], d['ndot'], d['nddot'], d['ecco'], d['argpo'],
            d['inclo'], d['mo'], d['no_kozai'], d['nodeo'])

# ----------------------------------------------------------------------
#                           INTEGRATION TEST
#
# This runs both new and old satellite objects against every example
# computation in the official `tcppver.out` test case file.  Instead of
# trying to parse the file, it instead re-generates it using Python
# satellite objects, then compares the resulting text with the file.

def test_satrec_against_tcppver_using_julian_dates():

    def invoke(satrec, tsince):
        whole, fraction = divmod(tsince / 1440.0, 1.0)
        jd = satrec.jdsatepoch + whole
        fr = satrec.jdsatepochF + fraction
        e, r, v = satrec.sgp4(jd, fr)
        assert e == satrec.error
        return e, r, v

    run_satellite_against_tcppver(Satrec.twoline2rv, invoke, [1,1,6,6,4,3,6])

def test_satrec_against_tcppver_using_tsince():

    def invoke(satrec, tsince):
        e, r, v = satrec.sgp4_tsince(tsince)
        assert e == satrec.error
        return e, r, v

    run_satellite_against_tcppver(Satrec.twoline2rv, invoke, [1,1,6,6,4,3,6])

def test_legacy_against_tcppver():

    def make_legacy_satellite(line1, line2):
        sat = io.twoline2rv(line1, line2, wgs72)
        return sat

    def run_legacy_sgp4(satrec, tsince):
        r, v = sgp4(satrec, tsince)
        return (satrec.error, satrec.error_message), r, v

    errs = [
        (1, 'mean eccentricity -0.001329 not within range 0.0 <= e < 1.0'),
        (1, 'mean eccentricity -0.001208 not within range 0.0 <= e < 1.0'),
        (6, 'mrt 0.996159 is less than 1.0'
         ' indicating the satellite has decayed'),
        (6, 'mrt 0.996252 is less than 1.0'
         ' indicating the satellite has decayed'),
        (4, 'semilatus rectum -0.103223 is less than zero'),
        (3, 'perturbed eccentricity -122.217193'
         ' not within range 0.0 <= e <= 1.0'),
        (6, 'mrt 0.830534 is less than 1.0'
         ' indicating the satellite has decayed'),
    ]

    run_satellite_against_tcppver(make_legacy_satellite, run_legacy_sgp4, errs)

def run_satellite_against_tcppver(twoline2rv, invoke, expected_errors):
    # Check whether this library can produce (at least roughly) the
    # output in tcppver.out.

    data = get_data(__name__, 'tcppver.out')
    data = data.replace(b'\r', b'')
    tcppver_lines = data.decode('ascii').splitlines(True)

    error_list = []
    actual_lines = list(generate_test_output(twoline2rv, invoke, error_list))

    assert len(tcppver_lines) == len(actual_lines) == 700

    previous_data_line = None
    linepairs = zip(tcppver_lines, actual_lines)

    for lineno, (expected_line, actual_line) in enumerate(linepairs, start=1):

        if actual_line == '(Use previous data line)':
            actual_line = ('       0.00000000' +
                           previous_data_line[17:107])

        # Compare the lines.  The first seven fields are printed
        # to very high precision, so we allow a small error due
        # to rounding differences; the rest are printed to lower
        # precision, and so can be compared textually.

        if 'xx' in actual_line:
            similar = (actual_line == expected_line)
        else:
            afields = actual_line.split()
            efields = expected_line.split()
            actual7 = [ float(a) for a in afields[:7] ]
            expected7 = [ float(e) for e in efields[:7] ]
            similar = (
                len(actual7) == len(expected7)
                and
                all(
                    -error < (a - e) < error
                     for a, e in zip(actual7, expected7)
                     )
                and
                afields[7:] == efields[7:]  # just compare text
                )

        if not similar:
            raise ValueError(
                'Line %d of output does not match:\n'
                '\n'
                'Expected: %r\n'
                'Got back: %r'
                % (lineno, expected_line, actual_line))

        if 'xx' not in actual_line:
            previous_data_line = actual_line

    # Make sure we produced the correct list of errors.
    assertEqual(error_list, expected_errors)

def generate_test_output(twoline2rv, invoke, error_list):
    """Generate lines like those in the test file tcppver.out.

    This iterates through the satellites in "SGP4-VER.TLE", which are
    each supplemented with a time start/stop/step over which we are
    supposed to print results.

    """
    data = get_data(__name__, 'SGP4-VER.TLE')
    tle_lines = iter(data.decode('ascii').splitlines())

    for line1 in tle_lines:

        if not line1.startswith('1'):
            continue

        line2 = next(tle_lines)
        satrec = twoline2rv(line1, line2)

        yield '%ld xx\n' % (satrec.satnum,)

        for line in generate_satellite_output(
                satrec, invoke, line2, error_list):
            yield line

def generate_satellite_output(satrec, invoke, line2, error_list):
    """Print a data line for each time in line2's start/stop/step field."""

    mu = wgs72.mu

    e, r, v = invoke(satrec, 0.0)
    if isnan(r[0]) and isnan(r[1]) and isnan(r[2]):
        error_list.append(e)
        yield '(Use previous data line)'
        return
    yield format_short_line(0.0, r, v)

    tstart, tend, tstep = (float(field) for field in line2[69:].split())

    tsince = tstart
    while tsince <= tend:
        if tsince == tstart == 0.0:
            tsince += tstep
            continue  # avoid duplicating the first line

        e, r, v = invoke(satrec, tsince)

        if e != 0 and e != (0, None):
            error_list.append(e)
            return
        yield format_long_line(satrec, tsince, mu, r, v)

        tsince += tstep

    if tsince - tend < tstep - 1e-6:  # do not miss last line!
        e, r, v = invoke(satrec, tend)
        if e != 0 and e != (0, None):
            error_list.append(e)
            return
        yield format_long_line(satrec, tend, mu, r, v)

def format_short_line(tsince, r, v):
    """Short line, using the same format string that testcpp.cpp uses."""

    return ' %16.8f %16.8f %16.8f %16.8f %12.9f %12.9f %12.9f\n' % (
        tsince, r[0], r[1], r[2], v[0], v[1], v[2])

def format_long_line(satrec, tsince, mu, r, v):
    """Long line, using the same format string that testcpp.cpp uses."""

    short = format_short_line(tsince, r, v).strip('\n')

    jd = satrec.jdsatepoch + satrec.jdsatepochF + tsince / 1440.0
    year, mon, day, hr, minute, sec = invjday(jd)

    (p, a, ecc, incl, node, argp, nu, m, arglat, truelon, lonper
     ) = rv2coe(r, v, mu)

    return short + (
        ' %14.6f %8.6f %10.5f %10.5f %10.5f %10.5f %10.5f'
        ' %5i%3i%3i %2i:%2i:%9.6f\n'
    ) % (
        a, ecc, incl*rad, node*rad, argp*rad, nu*rad,
        m*rad, year, mon, day, hr, minute, sec,
    )

# ----------------------------------------------------------------------
#                         NEW "OMM" FORMAT TESTS


# https://celestrak.com/satcat/tle.php?CATNR=5
VANGUARD_TLE = """\
VANGUARD 1              \n\
1 00005U 58002B   20287.20333880 -.00000016  00000-0 -22483-4 0  9998
2 00005  34.2443 225.5254 1845686 162.2516 205.2356 10.84869164218149
"""

# The MARIO satellite was chosen by reading through stations.txt looking
# for an element set with every field nonzero, since a zero in any field
# would hide errors in our unit conversion: zero times any conversion
# factor is zero.
MARIO_TLE = """\
MARIO                   \n\
1 55123U 98067UQ  23115.44827133  .00787702  29408-3  15680-2 0  9999
2 55123  51.6242 216.2930 0014649 331.8976  28.1241 15.99081912 18396
"""

# https://celestrak.org/NORAD/elements/gp.php?CATNR=55123&FORMAT=XML
MARIO_XML = """\
<?xml version="1.0" encoding="UTF-8"?>
<ndm xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://sanaregistry.org/r/ndmxml_unqualified/ndmxml-2.0.0-master-2.0.xsd">
<omm id="CCSDS_OMM_VERS" version="2.0">
<header><CREATION_DATE/><ORIGINATOR/></header><body><segment><metadata><OBJECT_NAME>MARIO</OBJECT_NAME><OBJECT_ID>1998-067UQ</OBJECT_ID><CENTER_NAME>EARTH</CENTER_NAME><REF_FRAME>TEME</REF_FRAME><TIME_SYSTEM>UTC</TIME_SYSTEM><MEAN_ELEMENT_THEORY>SGP4</MEAN_ELEMENT_THEORY></metadata><data><meanElements><EPOCH>2023-04-25T10:45:30.642912</EPOCH><MEAN_MOTION>15.99081912</MEAN_MOTION><ECCENTRICITY>.0014649</ECCENTRICITY><INCLINATION>51.6242</INCLINATION><RA_OF_ASC_NODE>216.2930</RA_OF_ASC_NODE><ARG_OF_PERICENTER>331.8976</ARG_OF_PERICENTER><MEAN_ANOMALY>28.1241</MEAN_ANOMALY></meanElements><tleParameters><EPHEMERIS_TYPE>0</EPHEMERIS_TYPE><CLASSIFICATION_TYPE>U</CLASSIFICATION_TYPE><NORAD_CAT_ID>55123</NORAD_CAT_ID><ELEMENT_SET_NO>999</ELEMENT_SET_NO><REV_AT_EPOCH>1839</REV_AT_EPOCH><BSTAR>.1568E-2</BSTAR><MEAN_MOTION_DOT>.787702E-2</MEAN_MOTION_DOT><MEAN_MOTION_DDOT>.29408E-3</MEAN_MOTION_DDOT></tleParameters></data></segment></body></omm>
</ndm>
"""

# https://celestrak.com/NORAD/elements/gp.php?CATNR=55123&FORMAT=CSV
MARIO_CSV = """\
OBJECT_NAME,OBJECT_ID,EPOCH,MEAN_MOTION,ECCENTRICITY,INCLINATION,RA_OF_ASC_NODE,ARG_OF_PERICENTER,MEAN_ANOMALY,EPHEMERIS_TYPE,CLASSIFICATION_TYPE,NORAD_CAT_ID,ELEMENT_SET_NO,REV_AT_EPOCH,BSTAR,MEAN_MOTION_DOT,MEAN_MOTION_DDOT
MARIO,1998-067UQ,2023-04-25T10:45:30.642912,15.99081912,.0014649,51.6242,216.2930,331.8976,28.1241,0,U,55123,999,1839,.1568E-2,.787702E-2,.29408E-3
"""

def test_omm_xml_matches_old_tle():
    line0, line1, line2 = MARIO_TLE.splitlines()
    sat1 = Satrec.twoline2rv(line1, line2)

    fields = next(omm.parse_xml(StringIO(MARIO_XML)))
    sat2 = Satrec()
    omm.initialize(sat2, fields)

    assert_satellites_match(sat1, sat2)

def test_omm_csv_matches_old_tle():
    line0, line1, line2 = MARIO_TLE.splitlines()
    sat1 = Satrec.twoline2rv(line1, line2)

    fields = next(omm.parse_csv(StringIO(MARIO_CSV)))
    sat2 = Satrec()
    omm.initialize(sat2, fields)

    assert_satellites_match(sat1, sat2)

def assert_satellites_match(sat1, sat2):
    for attr in dir(sat1):
        if attr.startswith('_'):
            continue
        value1 = getattr(sat1, attr, None)
        if value1 is None:
            continue
        if callable(value1):
            continue
        value2 = getattr(sat2, attr)
        assertEqual(value1, value2, '%s %r != %r' % (attr, value1, value2))

# Live example of OMM:
# https://celestrak.com/NORAD/elements/gp.php?INTDES=2020-025&FORMAT=JSON-PRETTY

def test_omm_export():
    line0, line1, line2 = VANGUARD_TLE.splitlines()
    sat = Satrec.twoline2rv(line1, line2)

    fields = export_omm(sat, 'VANGUARD 1')
    assertEqual(fields, {
        'ARG_OF_PERICENTER': 162.2516,
        'BSTAR': -2.2483e-05,
        'CENTER_NAME': 'EARTH',
        'CLASSIFICATION_TYPE': 'U',
        'ECCENTRICITY': 0.1845686,
        'ELEMENT_SET_NO': 999,
        'EPHEMERIS_TYPE': 0,
        'EPOCH': '2020-10-13T04:52:48.472320',
        'INCLINATION': 34.2443,
        'MEAN_ANOMALY': 205.2356,
        'MEAN_ELEMENT_THEORY': 'SGP4',
        'MEAN_MOTION': 10.84869164,
        'MEAN_MOTION_DDOT': 0.0,
        'MEAN_MOTION_DOT': -1.6e-07,
        'NORAD_CAT_ID': 5,
        'OBJECT_ID': '1958-002B',
        'OBJECT_NAME': 'VANGUARD 1',
        'RA_OF_ASC_NODE': 225.5254,
        'REF_FRAME': 'TEME',
        'REV_AT_EPOCH': 21814,
        'TIME_SYSTEM': 'UTC',
    })

# ----------------------------------------------------------------------

def load_tests(loader, tests, ignore):
    """Run our main documentation as a test, plus all test functions."""

    from sgp4.wulfgar import add_test_functions
    add_test_functions(loader, tests, __name__)

    # Python 2.6 formats floating-point numbers a bit differently and
    # breaks the doctest, so we only run the doctest on later versions.
    if sys.version_info >= (2, 7):

        def setUp(suite):
            suite.olddir = os.getcwd()
            os.chdir(os.path.dirname(__file__))
            suite.oldaccel = api.accelerated
            api.accelerated = True  # so doctest passes under 2.7
        def tearDown(suite):
            os.chdir(suite.olddir)
            api.accelerated = suite.oldaccel

        options = dict(optionflags=ELLIPSIS, setUp=setUp, tearDown=tearDown)
        tests.addTests(DocTestSuite('sgp4', **options))
        tests.addTests(DocTestSuite('sgp4.conveniences', **options))
        tests.addTests(DocTestSuite('sgp4.functions', **options))

    return tests

if __name__ == '__main__':
    main()
