use siderust::bodies::catalog;
use siderust::bodies::solar_system::{Moon, Sun};
use siderust::coordinates::centers::Geodetic;
use siderust::coordinates::frames::ECEF;
use siderust::coordinates::spherical::direction;
use siderust::event::azimuth::{
azimuth_crossings, azimuth_extrema, azimuth_periods, azimuth_ranges, in_azimuth_range,
outside_azimuth_range, AzimuthProvider, AzimuthQuery, SearchOpts,
};
use siderust::qtty::*;
use siderust::time::{Interval, ModifiedJulianDate};
fn greenwich() -> Geodetic<ECEF> {
Geodetic::<ECEF>::new(Degrees::new(0.0), Degrees::new(51.4769), Meters::new(0.0))
}
fn one_day() -> Interval<ModifiedJulianDate> {
Interval::new(
ModifiedJulianDate::try_new(Days::new(60000.0)).unwrap(),
ModifiedJulianDate::try_new(Days::new(60001.0)).unwrap(),
)
}
fn one_week() -> Interval<ModifiedJulianDate> {
Interval::new(
ModifiedJulianDate::try_new(Days::new(60000.0)).unwrap(),
ModifiedJulianDate::try_new(Days::new(60007.0)).unwrap(),
)
}
#[test]
fn sun_azimuth_at_in_valid_range() {
let az = Sun.azimuth_at(
&greenwich(),
ModifiedJulianDate::try_new(Days::new(60000.5)).unwrap(),
);
assert!(az.value() >= 0.0, "az must be ≥ 0 rad");
assert!(az.value() < std::f64::consts::TAU, "az must be < 2π rad");
}
#[test]
fn moon_azimuth_at_in_valid_range() {
let az = Moon.azimuth_at(
&greenwich(),
ModifiedJulianDate::try_new(Days::new(60000.5)).unwrap(),
);
assert!(az.value() >= 0.0);
assert!(az.value() < std::f64::consts::TAU);
}
#[test]
fn star_azimuth_at_in_valid_range() {
let sirius = &catalog::SIRIUS;
let az = sirius.azimuth_at(
&greenwich(),
ModifiedJulianDate::try_new(Days::new(60000.5)).unwrap(),
);
assert!(az.value() >= 0.0);
assert!(az.value() < std::f64::consts::TAU);
}
#[test]
fn star_and_icrs_direction_azimuth_agree() {
let sirius = &catalog::SIRIUS;
let dir = direction::ICRS::from(sirius);
let mjd = ModifiedJulianDate::try_new(Days::new(60000.3)).unwrap();
let az_star = sirius.azimuth_at(&greenwich(), mjd);
let az_dir = dir.azimuth_at(&greenwich(), mjd);
assert!(
(az_star.value() - az_dir.value()).abs() < 1e-10,
"Star and direction::ICRS must agree on azimuth"
);
}
#[test]
fn sun_crosses_south_once_per_day() {
let events = azimuth_crossings(
&Sun,
&greenwich(),
one_day(),
Degrees::new(180.0),
SearchOpts::default(),
);
assert!(
!events.is_empty(),
"should detect the Sun's southward meridian transit"
);
}
#[test]
fn sun_crosses_east_or_west_in_24h() {
let east = azimuth_crossings(
&Sun,
&greenwich(),
one_day(),
Degrees::new(90.0),
SearchOpts::default(),
);
let west = azimuth_crossings(
&Sun,
&greenwich(),
one_day(),
Degrees::new(270.0),
SearchOpts::default(),
);
assert!(
!east.is_empty() || !west.is_empty(),
"Sun should cross East or West bearing in 24h"
);
}
#[test]
fn moon_crossing_south_over_week() {
let events = azimuth_crossings(
&Moon,
&greenwich(),
one_week(),
Degrees::new(180.0),
SearchOpts::default(),
);
assert!(
!events.is_empty(),
"Moon should cross South bearing in a week"
);
}
#[test]
fn sun_azimuth_extrema_smoke() {
let exts = azimuth_extrema(&Sun, &greenwich(), one_day(), SearchOpts::default());
for e in &exts {
assert!(
e.azimuth.value() >= 0.0 && e.azimuth.value() < 360.0,
"extremum azimuth must be in [0°, 360°), got {}",
e.azimuth.value()
);
}
}
#[test]
fn star_azimuth_extrema_smoke() {
let sirius = &catalog::SIRIUS;
let exts = azimuth_extrema(sirius, &greenwich(), one_day(), SearchOpts::default());
for e in &exts {
assert!(
e.azimuth.value() >= 0.0 && e.azimuth.value() < 360.0,
"extremum azimuth must be in [0°, 360°), got {}",
e.azimuth.value()
);
}
}
#[test]
fn sun_in_eastern_half_non_empty() {
let query = AzimuthQuery {
observer: greenwich(),
window: one_day(),
min_azimuth: Degrees::new(90.0),
max_azimuth: Degrees::new(270.0),
opts: SearchOpts::default(),
correction_policy: siderust::astro::apparent::CorrectionPolicy::APPARENT,
};
let periods = Sun.azimuth_periods(&query);
assert!(
!periods.is_empty(),
"Sun should spend time in the eastern half (az 90–270°)"
);
}
#[test]
fn azimuth_periods_free_fn_matches_trait() {
let observer = greenwich();
let window = one_day();
let query = AzimuthQuery {
observer,
window,
min_azimuth: Degrees::new(90.0),
max_azimuth: Degrees::new(270.0),
opts: SearchOpts::default(),
correction_policy: siderust::astro::apparent::CorrectionPolicy::APPARENT,
};
let via_trait = Sun.azimuth_periods(&query);
let via_fn = azimuth_periods(&Sun, &query);
assert_eq!(
via_trait.len(),
via_fn.len(),
"trait and free-function must agree on period count"
);
}
#[test]
fn in_azimuth_range_equals_azimuth_ranges() {
let observer = greenwich();
let window = one_day();
let via_ranges = azimuth_ranges(
&Sun,
&observer,
window,
Degrees::new(90.0),
Degrees::new(270.0),
SearchOpts::default(),
);
let via_in = in_azimuth_range(
&Sun,
&observer,
window,
Degrees::new(90.0),
Degrees::new(270.0),
SearchOpts::default(),
);
assert_eq!(
via_ranges.len(),
via_in.len(),
"azimuth_ranges and in_azimuth_range must agree"
);
}
#[test]
fn outside_plus_inside_equals_window_sun() {
let observer = greenwich();
let window = one_day();
let inside = in_azimuth_range(
&Sun,
&observer,
window,
Degrees::new(90.0),
Degrees::new(270.0),
SearchOpts::default(),
);
let outside = outside_azimuth_range(
&Sun,
&observer,
window,
Degrees::new(90.0),
Degrees::new(270.0),
SearchOpts::default(),
);
let total: f64 = inside
.iter()
.chain(outside.iter())
.map(|p| p.length().value())
.sum();
let window_len = (window.end.raw() - window.start.raw()).value();
assert!(
(total - window_len).abs() < 1e-5,
"inside ({total:.6}) + outside must equal window ({window_len:.6})"
);
}
#[test]
fn outside_plus_inside_equals_window_moon() {
let observer = greenwich();
let window = one_week();
let inside = in_azimuth_range(
&Moon,
&observer,
window,
Degrees::new(90.0),
Degrees::new(270.0),
SearchOpts::default(),
);
let outside = outside_azimuth_range(
&Moon,
&observer,
window,
Degrees::new(90.0),
Degrees::new(270.0),
SearchOpts::default(),
);
let total: f64 = inside
.iter()
.chain(outside.iter())
.map(|p| p.length().value())
.sum();
let window_len = (window.end.raw() - window.start.raw()).value();
assert!(
(total - window_len).abs() < 1e-5,
"Moon: inside + outside must equal window"
);
}
#[test]
fn wrap_range_complement_covers_window() {
let observer = greenwich();
let window = one_week();
let inside = in_azimuth_range(
&Sun,
&observer,
window,
Degrees::new(340.0),
Degrees::new(20.0),
SearchOpts::default(),
);
let outside = outside_azimuth_range(
&Sun,
&observer,
window,
Degrees::new(340.0),
Degrees::new(20.0),
SearchOpts::default(),
);
let total: f64 = inside
.iter()
.chain(outside.iter())
.map(|p| p.length().value())
.sum();
let window_len = (window.end.raw() - window.start.raw()).value();
assert!(
(total - window_len).abs() < 1e-5,
"wrap-around inside + outside must cover the full window"
);
}