import pytest from datetime import datetime, timezone, timedelta from planetarytime import Body, EpochType, Moon, PlanetaryTime from planetarytime.exceptions import EpochUnavailableError from planetarytime.epoch import get_epoch_date # ── Moon dataclass ───────────────────────────────────────────────────────────── def test_body_getitem_returns_moon() -> None: assert isinstance(Body.MARS[0], Moon) def test_mars_first_moon_is_phobos() -> None: assert Body.MARS[0].name == "Phobos" def test_mars_second_moon_is_deimos() -> None: assert Body.MARS[1].name == "Deimos" def test_mars_index_out_of_range_raises() -> None: with pytest.raises(IndexError): Body.MARS[99] def test_mercury_has_no_moons() -> None: with pytest.raises(IndexError): Body.MERCURY[0] def test_tidally_locked_sols_per_year_is_one() -> None: phobos = Body.MARS[0] assert phobos.is_tidally_locked is True assert phobos.sols_per_year == 1 def test_moon_hours_per_sol_equals_rounded_rotation() -> None: phobos = Body.MARS[0] assert phobos.hours_per_sol == round(phobos.rotation_hours) def test_titan_has_contact_date() -> None: titan = Body.SATURN[0] assert titan.name == "Titan" assert titan.contact_date is not None def test_display_name_returns_moon_name() -> None: assert Body.MARS[0].display_name == "Phobos" # ── Epoch ────────────────────────────────────────────────────────────────────── def test_moon_discovery_epoch_date() -> None: phobos = Body.MARS[0] assert get_epoch_date(phobos, EpochType.DISCOVERY) == datetime(1877, 8, 18, tzinfo=timezone.utc) def test_moon_contact_epoch_date() -> None: titan = Body.SATURN[0] assert get_epoch_date(titan, EpochType.CONTACT) == datetime(2005, 1, 14, tzinfo=timezone.utc) def test_moon_contact_epoch_unavailable_raises() -> None: phobos = Body.MARS[0] with pytest.raises(EpochUnavailableError): get_epoch_date(phobos, EpochType.CONTACT) # ── PlanetaryTime with Moon ──────────────────────────────────────────────────── def test_planetary_time_from_earth_with_moon() -> None: phobos = Body.MARS[0] epoch_dt = get_epoch_date(phobos, EpochType.DISCOVERY) pt = PlanetaryTime.from_earth(epoch_dt, phobos, EpochType.DISCOVERY) assert pt.year == 0 assert pt.sol == 0 assert pt.hour == 0 def test_planetary_time_moon_one_sol_later_tidally_locked() -> None: # Phobos is tidally locked: sols_per_year == 1, so after 1 sol the year rolls over. phobos = Body.MARS[0] assert phobos.sols_per_year == 1 epoch_dt = get_epoch_date(phobos, EpochType.DISCOVERY) one_sol_later = epoch_dt + timedelta(hours=phobos.hours_per_sol) pt = PlanetaryTime.from_earth(one_sol_later, phobos, EpochType.DISCOVERY) assert pt.year == 1 assert pt.sol == 0 def test_planetary_time_moon_one_year_later() -> None: phobos = Body.MARS[0] epoch_dt = get_epoch_date(phobos, EpochType.DISCOVERY) one_year_seconds = phobos.sols_per_year * phobos.hours_per_sol * 3600 pt = PlanetaryTime.from_earth(epoch_dt + timedelta(seconds=one_year_seconds), phobos, EpochType.DISCOVERY) assert pt.year == 1 assert pt.sol == 0 def test_str_contains_moon_name() -> None: phobos = Body.MARS[0] epoch_dt = get_epoch_date(phobos, EpochType.DISCOVERY) pt = PlanetaryTime.from_earth(epoch_dt, phobos, EpochType.DISCOVERY) assert "Phobos" in str(pt) def test_repr_contains_moon_name() -> None: phobos = Body.MARS[0] epoch_dt = get_epoch_date(phobos, EpochType.DISCOVERY) pt = PlanetaryTime.from_earth(epoch_dt, phobos, EpochType.DISCOVERY) assert "Phobos" in repr(pt)