import numpy as np
import pytest

from gri_utils.conversion import ecef_to_eci, eci_to_ecef
from gri_utils.conversion.julian import JD_J2000, JD_UNIX_EPOCH

# Type alias for readability
XYZ = tuple[float, float, float]

# Earth radius at equator (meters)
ER_M = 6378137.0

# Test point: Equator at prime meridian in ECEF
_XYZ_EQUATOR_PRIME: XYZ = (ER_M, 0.0, 0.0)

# Test point: Equator at 90E in ECEF
_XYZ_EQUATOR_90E: XYZ = (0.0, ER_M, 0.0)

# Test point: North pole in ECEF (same in ECI)
_XYZ_NORTH_POLE: XYZ = (0.0, 0.0, 6356752.3142)

# Unix timestamp for J2000.0 epoch (2000-01-01 12:00:00 UTC)
UNIX_J2000 = (JD_J2000 - JD_UNIX_EPOCH) * 86400.0


def test_ecef_to_eci_basic() -> None:
    """Test basic ECEF to ECI conversion."""
    ecef = np.array(_XYZ_EQUATOR_PRIME)
    eci = ecef_to_eci(ecef, 0.0)

    # At Unix epoch, GMST is non-zero, so ECI should differ from ECEF
    # The result should have the same magnitude (rotation preserves length)
    assert np.isclose(np.linalg.norm(eci), np.linalg.norm(ecef))

    # Z component should be unchanged (rotation about Z-axis)
    assert np.isclose(eci[2], ecef[2])


def test_eci_to_ecef_basic() -> None:
    """Test basic ECI to ECEF conversion."""
    eci = np.array(_XYZ_EQUATOR_PRIME)
    ecef = eci_to_ecef(eci, 0.0)

    # Rotation preserves length
    assert np.isclose(np.linalg.norm(ecef), np.linalg.norm(eci))

    # Z component should be unchanged
    assert np.isclose(ecef[2], eci[2])


def test_north_pole_unchanged() -> None:
    """North pole should be unchanged by ECEF/ECI transformation."""
    ecef = np.array(_XYZ_NORTH_POLE)

    # Test at various times
    for unix_s in [0.0, 86400.0, UNIX_J2000]:
        eci = ecef_to_eci(ecef, unix_s)
        assert np.allclose(eci, ecef), f"ECI != ECEF at t={unix_s}"

        ecef_back = eci_to_ecef(eci, unix_s)
        assert np.allclose(ecef_back, ecef), f"Round-trip failed at t={unix_s}"


def test_roundtrip_single_point() -> None:
    """Test ECEF -> ECI -> ECEF roundtrip for single points."""
    test_points = [
        _XYZ_EQUATOR_PRIME,
        _XYZ_EQUATOR_90E,
        _XYZ_NORTH_POLE,
        (1e6, 2e6, 3e6),  # Arbitrary point
    ]
    test_times = [0.0, 86400.0, UNIX_J2000, -86400.0]

    for xyz in test_points:
        ecef = np.array(xyz)
        for unix_s in test_times:
            eci = ecef_to_eci(ecef, unix_s)
            ecef_back = eci_to_ecef(eci, unix_s)
            assert np.allclose(ecef_back, ecef, rtol=1e-12), (
                f"Round-trip failed for {xyz} at t={unix_s}"
            )


def test_roundtrip_eci_to_ecef() -> None:
    """Test ECI -> ECEF -> ECI roundtrip."""
    eci = np.array([1e7, 2e7, 3e6])
    unix_s = UNIX_J2000

    ecef = eci_to_ecef(eci, unix_s)
    eci_back = ecef_to_eci(ecef, unix_s)

    assert np.allclose(eci_back, eci, rtol=1e-12)


def test_rotation_direction() -> None:
    """Test that rotation direction is correct.

    As time increases, Earth rotates eastward (counterclockwise when viewed
    from above the North pole). In ECI, the Earth-fixed X-axis moves eastward,
    so a point fixed in ECEF should move counterclockwise in ECI.
    """
    ecef = np.array(_XYZ_EQUATOR_PRIME)  # X-axis in ECEF

    # Get ECI at two times, 6 hours apart (~90 degrees of rotation)
    t1 = 0.0
    t2 = 6 * 3600.0  # 6 hours later

    eci1 = ecef_to_eci(ecef, t1)
    eci2 = ecef_to_eci(ecef, t2)

    # After 6 hours, the point should have rotated ~90 degrees counterclockwise
    # in the X-Y plane. Y component should increase (become positive).
    # The Z component of cross product eci1 x eci2 should be positive (counterclockwise)
    cross = np.cross(eci1, eci2)
    assert cross[2] > 0, "Rotation should be counterclockwise (positive Z)"


def test_one_sidereal_day() -> None:
    """After one sidereal day, ECI coordinates should nearly repeat."""
    ecef = np.array(_XYZ_EQUATOR_PRIME)

    # Sidereal day is ~86164.1 seconds
    sidereal_day_s = 86164.090530833

    eci1 = ecef_to_eci(ecef, 0.0)
    eci2 = ecef_to_eci(ecef, sidereal_day_s)

    # Should be very close (within ~1 meter for Earth-radius distances)
    assert np.allclose(eci1, eci2, atol=1.0), (
        f"ECI should repeat after sidereal day: {eci1} vs {eci2}"
    )


def test_sequence_input() -> None:
    """Test that sequence inputs work correctly."""
    # Test with tuple input
    ecef_tuple = _XYZ_EQUATOR_PRIME
    eci = ecef_to_eci(ecef_tuple, 0.0)
    assert isinstance(eci, np.ndarray)
    assert eci.shape == (3,)

    # Test with list input
    ecef_list = list(_XYZ_EQUATOR_PRIME)
    eci_list = ecef_to_eci(ecef_list, 0.0)
    assert np.allclose(eci, eci_list)


def test_vectorized_points() -> None:
    """Test with batched (N, 3) point input."""
    ecefs = np.array(
        [
            _XYZ_EQUATOR_PRIME,
            _XYZ_EQUATOR_90E,
            _XYZ_NORTH_POLE,
        ],
    )
    unix_s = UNIX_J2000

    ecis = ecef_to_eci(ecefs, unix_s)

    assert ecis.shape == (3, 3), f"Expected shape (3, 3), got {ecis.shape}"

    # Verify each point individually
    for i in range(3):
        eci_single = ecef_to_eci(ecefs[i], unix_s)
        assert np.allclose(ecis[i], eci_single)


def test_vectorized_times() -> None:
    """Test with array of times (one time per point)."""
    ecefs = np.array(
        [
            _XYZ_EQUATOR_PRIME,
            _XYZ_EQUATOR_PRIME,
            _XYZ_EQUATOR_PRIME,
        ],
    )
    times = np.array([0.0, 3600.0, 7200.0])  # 0, 1, 2 hours

    ecis = ecef_to_eci(ecefs, times)

    assert ecis.shape == (3, 3)

    # Each point should be at a different ECI position
    # (since same ECEF but different times)
    assert not np.allclose(ecis[0], ecis[1])
    assert not np.allclose(ecis[1], ecis[2])


def test_vectorized_roundtrip() -> None:
    """Test round-trip with batched inputs."""
    ecefs = np.array(
        [
            [1e6, 2e6, 3e6],
            [4e6, 5e6, 6e6],
            [-1e6, -2e6, 1e6],
        ],
    )
    times = np.array([0.0, 86400.0, UNIX_J2000])

    ecis = ecef_to_eci(ecefs, times)
    ecefs_back = eci_to_ecef(ecis, times)

    assert np.allclose(ecefs_back, ecefs, rtol=1e-12)


@pytest.mark.parametrize(
    "unix_s",
    [0.0, 86400.0, -86400.0, UNIX_J2000, 1e9],
)
def test_magnitude_preserved(unix_s: float) -> None:
    """Test that transformation preserves vector magnitude."""
    ecef = np.array([1e7, 2e7, 3e6])

    eci = ecef_to_eci(ecef, unix_s)
    assert np.isclose(np.linalg.norm(eci), np.linalg.norm(ecef))

    ecef_back = eci_to_ecef(eci, unix_s)
    assert np.isclose(np.linalg.norm(ecef_back), np.linalg.norm(ecef))
