use crate::*;
use std::time::Duration;

#[test]
fn ephemeris_request_has_a_compact_display() {
    let instant = Instant::new(JulianDay::from_days(2_451_545.0), TimeScale::Tt);
    let request = EphemerisRequest::new(CelestialBody::Mars, instant);
    let request = EphemerisRequest {
        observer: Some(ObserverLocation::new(
            Latitude::from_degrees(51.5),
            Longitude::from_degrees(-0.1),
            None,
        )),
        ..request
    };

    assert_eq!(request.to_string(), request.summary_line());
    assert_eq!(
            request.summary_line(),
            "body=Mars; instant=JD 2451545 TT; frame=Ecliptic; zodiac=Tropical; apparent=Mean; observer=latitude=51.5°, longitude=359.9°, elevation=n/a"
        );
    assert!(request.summary_line().contains("body=Mars"));
    assert!(request.summary_line().contains("observer="));
}

#[test]
fn request_policy_helpers_reject_unsupported_shapes() {
    let time_scale_request = EphemerisRequest {
        body: CelestialBody::Sun,
        instant: Instant::new(JulianDay::from_days(2451545.0), TimeScale::Utc),
        observer: Some(ObserverLocation::new(
            Latitude::from_degrees(51.5),
            Longitude::from_degrees(-0.1),
            Some(45.0),
        )),
        frame: CoordinateFrame::Equatorial,
        zodiac_mode: ZodiacMode::Tropical,
        apparent: Apparentness::Apparent,
    };
    let conversion = TimeScaleConversion::new(TimeScale::Utc, TimeScale::Tt, 64.184);
    assert!(time_scale_request
        .validate_time_scale_conversion(conversion)
        .is_ok());
    let converted = time_scale_request
        .clone()
        .with_time_scale_conversion(conversion)
        .expect("UTC request should convert with the caller-supplied policy");
    assert_eq!(converted.instant.scale, TimeScale::Tt);
    assert_eq!(converted.body, time_scale_request.body);
    assert_eq!(converted.observer, time_scale_request.observer);
    assert_eq!(converted.frame, time_scale_request.frame);
    assert_eq!(converted.zodiac_mode, time_scale_request.zodiac_mode);
    assert_eq!(converted.apparent, time_scale_request.apparent);

    let checked_offset = time_scale_request
        .clone()
        .with_instant_time_scale_offset_checked(TimeScale::Tt, 64.184)
        .expect("UTC request should accept the checked offset helper");
    assert_eq!(checked_offset.instant.scale, TimeScale::Tt);
    assert_eq!(checked_offset.body, time_scale_request.body);
    assert_eq!(checked_offset.observer, time_scale_request.observer);
    assert_eq!(checked_offset.frame, time_scale_request.frame);
    assert_eq!(checked_offset.zodiac_mode, time_scale_request.zodiac_mode);
    assert_eq!(checked_offset.apparent, time_scale_request.apparent);

    let tt_from_tdb_request = EphemerisRequest {
        instant: Instant::new(JulianDay::from_days(2451545.0), TimeScale::Tdb),
        ..time_scale_request.clone()
    };
    let tt_from_tdb = tt_from_tdb_request
        .clone()
        .with_tt_from_tdb(-0.001_657)
        .expect("TDB request should convert back to TT with a caller-supplied offset");
    assert_eq!(tt_from_tdb.instant.scale, TimeScale::Tt);
    assert_eq!(tt_from_tdb.body, tt_from_tdb_request.body);
    assert_eq!(tt_from_tdb.observer, tt_from_tdb_request.observer);
    assert_eq!(tt_from_tdb.frame, tt_from_tdb_request.frame);
    assert_eq!(tt_from_tdb.zodiac_mode, tt_from_tdb_request.zodiac_mode);
    assert_eq!(tt_from_tdb.apparent, tt_from_tdb_request.apparent);

    let tt_from_tdb_signed = tt_from_tdb_request
        .clone()
        .with_tt_from_tdb_signed(-0.001_657)
        .expect("TDB request should convert back to TT with a signed offset");
    assert_eq!(tt_from_tdb_signed.instant.scale, TimeScale::Tt);
    assert_eq!(tt_from_tdb_signed.body, tt_from_tdb_request.body);
    assert_eq!(tt_from_tdb_signed.observer, tt_from_tdb_request.observer);
    assert_eq!(tt_from_tdb_signed.frame, tt_from_tdb_request.frame);
    assert_eq!(
        tt_from_tdb_signed.zodiac_mode,
        tt_from_tdb_request.zodiac_mode
    );
    assert_eq!(tt_from_tdb_signed.apparent, tt_from_tdb_request.apparent);

    let tt_from_tdb_unsigned = tt_from_tdb_request
        .clone()
        .with_tt_from_tdb(0.001_657)
        .expect("TDB request should convert back to TT with a duration offset");
    assert_eq!(tt_from_tdb_unsigned.instant.scale, TimeScale::Tt);
    assert_eq!(tt_from_tdb_unsigned.body, tt_from_tdb_request.body);
    assert_eq!(tt_from_tdb_unsigned.observer, tt_from_tdb_request.observer);
    assert_eq!(tt_from_tdb_unsigned.frame, tt_from_tdb_request.frame);
    assert_eq!(
        tt_from_tdb_unsigned.zodiac_mode,
        tt_from_tdb_request.zodiac_mode
    );
    assert_eq!(tt_from_tdb_unsigned.apparent, tt_from_tdb_request.apparent);

    let tt_from_ut1_request = EphemerisRequest {
        instant: Instant::new(JulianDay::from_days(2451545.0), TimeScale::Ut1),
        ..time_scale_request.clone()
    };
    let tt_from_ut1 = tt_from_ut1_request
        .clone()
        .with_tt_from_ut1_signed(64.184)
        .expect("UT1 request should convert to TT with a signed offset");
    assert_eq!(tt_from_ut1.instant.scale, TimeScale::Tt);
    assert_eq!(tt_from_ut1.body, tt_from_ut1_request.body);
    assert_eq!(tt_from_ut1.observer, tt_from_ut1_request.observer);
    assert_eq!(tt_from_ut1.frame, tt_from_ut1_request.frame);
    assert_eq!(tt_from_ut1.zodiac_mode, tt_from_ut1_request.zodiac_mode);
    assert_eq!(tt_from_ut1.apparent, tt_from_ut1_request.apparent);

    let tt_from_ut1_unsigned = tt_from_ut1_request
        .clone()
        .with_tt_from_ut1(Duration::from_secs_f64(64.184))
        .expect("UT1 request should convert to TT with a duration offset");
    assert_eq!(tt_from_ut1_unsigned.instant.scale, TimeScale::Tt);
    assert_eq!(tt_from_ut1_unsigned.body, tt_from_ut1_request.body);
    assert_eq!(tt_from_ut1_unsigned.observer, tt_from_ut1_request.observer);
    assert_eq!(tt_from_ut1_unsigned.frame, tt_from_ut1_request.frame);
    assert_eq!(
        tt_from_ut1_unsigned.zodiac_mode,
        tt_from_ut1_request.zodiac_mode
    );
    assert_eq!(tt_from_ut1_unsigned.apparent, tt_from_ut1_request.apparent);

    let tt_from_utc_request = EphemerisRequest {
        instant: Instant::new(JulianDay::from_days(2451545.0), TimeScale::Utc),
        ..time_scale_request.clone()
    };
    let tt_from_utc = tt_from_utc_request
        .clone()
        .with_tt_from_utc_signed(64.184)
        .expect("UTC request should convert to TT with a signed offset");
    assert_eq!(tt_from_utc.instant.scale, TimeScale::Tt);
    assert_eq!(tt_from_utc.body, tt_from_utc_request.body);
    assert_eq!(tt_from_utc.observer, tt_from_utc_request.observer);
    assert_eq!(tt_from_utc.frame, tt_from_utc_request.frame);
    assert_eq!(tt_from_utc.zodiac_mode, tt_from_utc_request.zodiac_mode);
    assert_eq!(tt_from_utc.apparent, tt_from_utc_request.apparent);

    let tdb_from_tt_request = EphemerisRequest {
        instant: Instant::new(JulianDay::from_days(2451545.0), TimeScale::Tt),
        ..time_scale_request.clone()
    };
    let tdb_from_tt = tdb_from_tt_request
        .clone()
        .with_tdb_from_tt_signed(-0.001_657)
        .expect("TT request should convert to TDB with a signed offset");
    assert_eq!(tdb_from_tt.instant.scale, TimeScale::Tdb);
    assert_eq!(tdb_from_tt.body, tdb_from_tt_request.body);
    assert_eq!(tdb_from_tt.observer, tdb_from_tt_request.observer);
    assert_eq!(tdb_from_tt.frame, tdb_from_tt_request.frame);
    assert_eq!(tdb_from_tt.zodiac_mode, tdb_from_tt_request.zodiac_mode);
    assert_eq!(tdb_from_tt.apparent, tdb_from_tt_request.apparent);

    let tdb_from_tt_unsigned = tdb_from_tt_request
        .clone()
        .with_tdb_from_tt(Duration::from_secs_f64(0.001_657))
        .expect("TT request should convert to TDB with a duration offset");
    assert_eq!(tdb_from_tt_unsigned.instant.scale, TimeScale::Tdb);
    assert_eq!(tdb_from_tt_unsigned.body, tdb_from_tt_request.body);
    assert_eq!(tdb_from_tt_unsigned.observer, tdb_from_tt_request.observer);
    assert_eq!(tdb_from_tt_unsigned.frame, tdb_from_tt_request.frame);
    assert_eq!(
        tdb_from_tt_unsigned.zodiac_mode,
        tdb_from_tt_request.zodiac_mode
    );
    assert_eq!(tdb_from_tt_unsigned.apparent, tdb_from_tt_request.apparent);

    let tdb_from_ut1_request = EphemerisRequest {
        instant: Instant::new(JulianDay::from_days(2451545.0), TimeScale::Ut1),
        ..time_scale_request.clone()
    };
    let tdb_from_ut1 = tdb_from_ut1_request
        .clone()
        .with_tdb_from_ut1_signed(Duration::from_secs_f64(64.184), -0.001_657)
        .expect("UT1 request should convert to TDB with caller-supplied offsets");
    assert_eq!(tdb_from_ut1.instant.scale, TimeScale::Tdb);
    assert_eq!(tdb_from_ut1.body, tdb_from_ut1_request.body);
    assert_eq!(tdb_from_ut1.observer, tdb_from_ut1_request.observer);
    assert_eq!(tdb_from_ut1.frame, tdb_from_ut1_request.frame);
    assert_eq!(tdb_from_ut1.zodiac_mode, tdb_from_ut1_request.zodiac_mode);
    assert_eq!(tdb_from_ut1.apparent, tdb_from_ut1_request.apparent);

    let tdb_from_ut1_unsigned = tdb_from_ut1_request
        .clone()
        .with_tdb_from_ut1(
            Duration::from_secs_f64(64.184),
            Duration::from_secs_f64(0.001_657),
        )
        .expect("UT1 request should convert to TDB with duration offsets");
    assert_eq!(tdb_from_ut1_unsigned.instant.scale, TimeScale::Tdb);
    assert_eq!(tdb_from_ut1_unsigned.body, tdb_from_ut1_request.body);
    assert_eq!(
        tdb_from_ut1_unsigned.observer,
        tdb_from_ut1_request.observer
    );
    assert_eq!(tdb_from_ut1_unsigned.frame, tdb_from_ut1_request.frame);
    assert_eq!(
        tdb_from_ut1_unsigned.zodiac_mode,
        tdb_from_ut1_request.zodiac_mode
    );
    assert_eq!(
        tdb_from_ut1_unsigned.apparent,
        tdb_from_ut1_request.apparent
    );

    let tdb_from_utc_request = EphemerisRequest {
        instant: Instant::new(JulianDay::from_days(2451545.0), TimeScale::Utc),
        ..time_scale_request.clone()
    };
    let tdb_from_utc = tdb_from_utc_request
        .clone()
        .with_tdb_from_utc_signed(Duration::from_secs_f64(64.184), -0.001_657)
        .expect("UTC request should convert to TDB with caller-supplied offsets");
    assert_eq!(tdb_from_utc.instant.scale, TimeScale::Tdb);
    assert_eq!(tdb_from_utc.body, tdb_from_utc_request.body);
    assert_eq!(tdb_from_utc.observer, tdb_from_utc_request.observer);
    assert_eq!(tdb_from_utc.frame, tdb_from_utc_request.frame);
    assert_eq!(tdb_from_utc.zodiac_mode, tdb_from_utc_request.zodiac_mode);
    assert_eq!(tdb_from_utc.apparent, tdb_from_utc_request.apparent);

    let tdb_from_utc_unsigned = tdb_from_utc_request
        .clone()
        .with_tdb_from_utc(
            Duration::from_secs_f64(64.184),
            Duration::from_secs_f64(0.001_657),
        )
        .expect("UTC request should convert to TDB with duration offsets");
    assert_eq!(tdb_from_utc_unsigned.instant.scale, TimeScale::Tdb);
    assert_eq!(tdb_from_utc_unsigned.body, tdb_from_utc_request.body);
    assert_eq!(
        tdb_from_utc_unsigned.observer,
        tdb_from_utc_request.observer
    );
    assert_eq!(tdb_from_utc_unsigned.frame, tdb_from_utc_request.frame);
    assert_eq!(
        tdb_from_utc_unsigned.zodiac_mode,
        tdb_from_utc_request.zodiac_mode
    );
    assert_eq!(
        tdb_from_utc_unsigned.apparent,
        tdb_from_utc_request.apparent
    );

    let error = time_scale_request
        .clone()
        .with_instant_time_scale_offset_checked(TimeScale::Tt, f64::NAN)
        .expect_err("non-finite offsets should be rejected at the request layer");
    assert_eq!(error, TimeScaleConversionError::NonFiniteOffset);

    let error = time_scale_request
        .validate_time_scale_conversion(TimeScaleConversion::new(
            TimeScale::Tt,
            TimeScale::Tt,
            64.184,
        ))
        .expect_err("mismatched source scales should fail validation before retagging");
    assert_eq!(
        error,
        TimeScaleConversionError::Expected {
            expected: TimeScale::Tt,
            actual: TimeScale::Utc
        }
    );
    let error = validate_request_policy(
        &time_scale_request,
        "toy backend",
        &[TimeScale::Tt],
        &[CoordinateFrame::Ecliptic],
        true,
        false,
    )
    .expect_err("UTC should be rejected when only TT is supported");
    assert_eq!(error.kind, EphemerisErrorKind::UnsupportedTimeScale);
    assert_eq!(
        error.message,
        "toy backend expects one of [TT] for request instants"
    );

    let frame_request = EphemerisRequest {
        instant: Instant::new(JulianDay::from_days(2451545.0), TimeScale::Tt),
        observer: None,
        frame: CoordinateFrame::Equatorial,
        apparent: Apparentness::Mean,
        ..time_scale_request.clone()
    };
    let error = validate_request_policy(
        &frame_request,
        "toy backend",
        &[TimeScale::Tt],
        &[CoordinateFrame::Ecliptic],
        true,
        false,
    )
    .expect_err("equatorial frame should be rejected when only ecliptic is supported");
    assert_eq!(error.kind, EphemerisErrorKind::UnsupportedCoordinateFrame);
    assert_eq!(
        error.message,
        "toy backend only returns [Ecliptic] coordinates"
    );

    let apparent_request = EphemerisRequest {
        frame: CoordinateFrame::Ecliptic,
        apparent: Apparentness::Apparent,
        observer: None,
        ..frame_request.clone()
    };
    let error = validate_request_policy(
        &apparent_request,
        "toy backend",
        &[TimeScale::Tt],
        &[CoordinateFrame::Ecliptic],
        true,
        false,
    )
    .expect_err("apparent requests should be rejected when only mean output is supported");
    assert_eq!(error.kind, EphemerisErrorKind::UnsupportedApparentness);
    assert_eq!(
            error.message,
            "toy backend currently returns mean geometric coordinates only; apparent corrections are not implemented"
        );

    let invalid_observer_apparent_request = EphemerisRequest {
        observer: Some(ObserverLocation::new(
            Latitude::from_degrees(95.0),
            Longitude::from_degrees(-0.1),
            Some(45.0),
        )),
        ..apparent_request.clone()
    };
    let unsupported_apparent_metadata = BackendMetadata {
        id: BackendId::new("toy backend"),
        version: "0.1.0".to_string(),
        family: BackendFamily::Algorithmic,
        provenance: BackendProvenance::new("toy backend"),
        nominal_range: TimeRange::new(None, None),
        supported_time_scales: vec![TimeScale::Tt],
        body_claims: vec![CelestialBody::Sun.into()],
        supported_frames: vec![CoordinateFrame::Ecliptic],
        capabilities: BackendCapabilities {
            geocentric: true,
            topocentric: false,
            apparent: false,
            mean: true,
            batch: true,
            native_sidereal: false,
        },
        accuracy: AccuracyClass::Approximate,
        deterministic: true,
        offline: true,
    };
    let error = validate_request_against_metadata(
        &invalid_observer_apparent_request,
        &unsupported_apparent_metadata,
    )
    .expect_err("unsupported apparentness should be reported before observer validation");
    assert_eq!(error.kind, EphemerisErrorKind::UnsupportedApparentness);
    assert_eq!(
            error.message,
            "toy backend currently returns mean geometric coordinates only; apparent corrections are not implemented"
        );

    let batch_seed_request = EphemerisRequest {
        apparent: Apparentness::Mean,
        ..apparent_request.clone()
    };
    let batch_error = validate_requests_against_metadata(
            &[batch_seed_request, invalid_observer_apparent_request.clone()],
            &unsupported_apparent_metadata,
        )
        .expect_err("batch validation should report unsupported apparentness before invalid observer validation");
    assert_eq!(
        batch_error.kind,
        EphemerisErrorKind::UnsupportedApparentness
    );
    assert_eq!(
            batch_error.message,
            "batch request 2: toy backend currently returns mean geometric coordinates only; apparent corrections are not implemented"
        );

    let mean_request = EphemerisRequest {
        apparent: Apparentness::Mean,
        ..apparent_request.clone()
    };
    let error = validate_request_policy(
        &mean_request,
        "toy backend",
        &[TimeScale::Tt],
        &[CoordinateFrame::Ecliptic],
        false,
        true,
    )
    .expect_err("mean requests should be rejected when only apparent output is supported");
    assert_eq!(error.kind, EphemerisErrorKind::UnsupportedApparentness);
    assert_eq!(
            error.message,
            "toy backend currently returns apparent coordinates only; mean geometric coordinates are not implemented"
        );

    let metadata = BackendMetadata {
        id: BackendId::new("toy backend"),
        version: "0.1.0".to_string(),
        family: BackendFamily::Algorithmic,
        provenance: BackendProvenance::new("toy backend"),
        nominal_range: TimeRange::new(None, None),
        supported_time_scales: vec![TimeScale::Tt],
        body_claims: vec![CelestialBody::Sun.into()],
        supported_frames: vec![CoordinateFrame::Ecliptic],
        capabilities: BackendCapabilities {
            geocentric: true,
            topocentric: false,
            apparent: false,
            mean: true,
            batch: true,
            native_sidereal: false,
        },
        accuracy: AccuracyClass::Approximate,
        deterministic: true,
        offline: true,
    };

    let frame_error = validate_request_against_metadata(&frame_request, &metadata)
        .expect_err("equatorial requests should still be rejected through metadata preflight");
    assert_eq!(
        frame_error.kind,
        EphemerisErrorKind::UnsupportedCoordinateFrame
    );
    assert_eq!(
        frame_error.message,
        "toy backend only returns [Ecliptic] coordinates"
    );

    let metadata_frame_error = metadata
        .validate_request(&frame_request)
        .expect_err("metadata request validation should match the shared preflight");
    assert_eq!(metadata_frame_error.kind, frame_error.kind);
    assert_eq!(metadata_frame_error.message, frame_error.message);

    let topocentric_request = EphemerisRequest {
        observer: Some(ObserverLocation::new(
            Latitude::from_degrees(51.5),
            Longitude::from_degrees(-0.1),
            Some(45.0),
        )),
        apparent: Apparentness::Mean,
        ..apparent_request.clone()
    };
    let error = validate_request_against_metadata(&topocentric_request, &metadata)
        .expect_err("metadata preflight should reject observer-bearing geocentric requests");
    assert_eq!(error.kind, EphemerisErrorKind::UnsupportedObserver);
    assert!(error.message.contains("toy backend is geocentric only"));
    assert!(error.message.contains(
        &topocentric_request
            .observer
            .as_ref()
            .unwrap()
            .summary_line()
    ));

    let invalid_observer_request = EphemerisRequest {
        observer: Some(ObserverLocation::new(
            Latitude::from_degrees(95.0),
            Longitude::from_degrees(-0.1),
            Some(45.0),
        )),
        ..apparent_request.clone()
    };
    let routing_metadata = BackendMetadata {
        family: BackendFamily::Composite,
        capabilities: BackendCapabilities {
            topocentric: true,
            ..metadata.capabilities.clone()
        },
        ..metadata.clone()
    };
    let routing_error =
        validate_request_against_metadata(&invalid_observer_request, &routing_metadata)
            .expect_err("routing metadata should still reject invalid observer locations");
    assert_eq!(routing_error.kind, EphemerisErrorKind::InvalidObserver);
    assert!(routing_error
        .message
        .contains("request received invalid observer location"));

    let geocentric_only_request = EphemerisRequest::new(
        CelestialBody::Sun,
        Instant::new(JulianDay::from_days(2451545.0), TimeScale::Tt),
    );
    let geocentric_only_invalid_observer_request = EphemerisRequest {
        observer: Some(ObserverLocation::new(
            Latitude::from_degrees(95.0),
            Longitude::from_degrees(-0.1),
            Some(45.0),
        )),
        ..geocentric_only_request.clone()
    };
    let geocentric_only_error = validate_request_against_metadata(
            &geocentric_only_invalid_observer_request,
            &metadata,
        )
        .expect_err("geocentric-only metadata should still reject malformed observer locations as invalid input");
    assert_eq!(
        geocentric_only_error.kind,
        EphemerisErrorKind::InvalidObserver
    );
    assert!(geocentric_only_error
        .message
        .contains("request received invalid observer location"));

    let geocentric_only_batch_error = validate_requests_against_metadata(
        &[
            geocentric_only_request.clone(),
            geocentric_only_invalid_observer_request.clone(),
        ],
        &metadata,
    )
    .expect_err(
        "batch metadata should preserve invalid observer precedence for geocentric-only requests",
    );
    assert_eq!(
        geocentric_only_batch_error.kind,
        EphemerisErrorKind::InvalidObserver
    );
    assert!(geocentric_only_batch_error
        .message
        .contains("batch request 2: request received invalid observer location"));

    let invalid_observer_frame_request = EphemerisRequest {
        observer: Some(ObserverLocation::new(
            Latitude::from_degrees(95.0),
            Longitude::from_degrees(-0.1),
            Some(45.0),
        )),
        ..frame_request.clone()
    };
    let invalid_observer_frame_error =
        validate_request_against_metadata(&invalid_observer_frame_request, &metadata)
            .expect_err("frame policy should still win before malformed observer validation");
    assert_eq!(
        invalid_observer_frame_error.kind,
        EphemerisErrorKind::UnsupportedCoordinateFrame
    );
    assert_eq!(
        invalid_observer_frame_error.message,
        "toy backend only returns [Ecliptic] coordinates"
    );

    let invalid_observer_time_scale_request = EphemerisRequest {
        instant: Instant::new(JulianDay::from_days(2451545.0), TimeScale::Tdb),
        observer: Some(ObserverLocation::new(
            Latitude::from_degrees(95.0),
            Longitude::from_degrees(-0.1),
            Some(45.0),
        )),
        ..geocentric_only_request.clone()
    };
    let invalid_observer_time_scale_error =
        validate_request_against_metadata(&invalid_observer_time_scale_request, &metadata)
            .expect_err("time-scale policy should still win before malformed observer validation");
    assert_eq!(
        invalid_observer_time_scale_error.kind,
        EphemerisErrorKind::UnsupportedTimeScale
    );
    assert_eq!(
        invalid_observer_time_scale_error.message,
        "toy backend expects one of [TT] for request instants"
    );

    let invalid_observer_apparent_request = EphemerisRequest {
        observer: Some(ObserverLocation::new(
            Latitude::from_degrees(95.0),
            Longitude::from_degrees(-0.1),
            Some(45.0),
        )),
        ..apparent_request.clone()
    };
    let invalid_observer_apparent_error =
        validate_request_against_metadata(&invalid_observer_apparent_request, &metadata)
            .expect_err("apparentness should still win before malformed observer validation");
    assert_eq!(
        invalid_observer_apparent_error.kind,
        EphemerisErrorKind::UnsupportedApparentness
    );
    assert_eq!(
            invalid_observer_apparent_error.message,
            "toy backend currently returns mean geometric coordinates only; apparent corrections are not implemented"
        );

    let invalid_observer_frame_batch_error = validate_requests_against_metadata(
        &[
            geocentric_only_request.clone(),
            invalid_observer_frame_request.clone(),
        ],
        &metadata,
    )
    .expect_err(
        "batch metadata should preserve frame precedence before invalid observer validation",
    );
    assert_eq!(
        invalid_observer_frame_batch_error.kind,
        EphemerisErrorKind::UnsupportedCoordinateFrame
    );
    assert_eq!(
        invalid_observer_frame_batch_error.message,
        "batch request 2: toy backend only returns [Ecliptic] coordinates"
    );

    let unsupported_body_request = EphemerisRequest {
        body: CelestialBody::Mars,
        frame: CoordinateFrame::Ecliptic,
        apparent: Apparentness::Mean,
        observer: None,
        ..frame_request.clone()
    };
    let error = validate_request_against_metadata(&unsupported_body_request, &metadata)
        .expect_err("metadata preflight should reject bodies outside the declared coverage");
    assert_eq!(error.kind, EphemerisErrorKind::UnsupportedBody);
    assert_eq!(error.message, "toy backend does not support Mars");

    let unsupported_body_with_invalid_observer_request = EphemerisRequest {
        observer: Some(ObserverLocation::new(
            Latitude::from_degrees(95.0),
            Longitude::from_degrees(-0.1),
            Some(45.0),
        )),
        ..unsupported_body_request.clone()
    };
    let unsupported_body_with_invalid_observer_error = validate_request_against_metadata(
        &unsupported_body_with_invalid_observer_request,
        &metadata,
    )
    .expect_err("observer validation should win before unsupported body coverage is reported");
    assert_eq!(
        unsupported_body_with_invalid_observer_error.kind,
        EphemerisErrorKind::InvalidObserver
    );
    assert!(unsupported_body_with_invalid_observer_error
        .message
        .contains("request received invalid observer location"));

    let unsupported_body_batch_error = validate_requests_against_metadata(
        &[
            geocentric_only_request.clone(),
            unsupported_body_with_invalid_observer_request.clone(),
        ],
        &metadata,
    )
    .expect_err(
        "batch metadata should preserve invalid observer precedence over unsupported bodies",
    );
    assert_eq!(
        unsupported_body_batch_error.kind,
        EphemerisErrorKind::InvalidObserver
    );
    assert!(unsupported_body_batch_error
        .message
        .contains("batch request 2: request received invalid observer location"));

    let sidereal_request = EphemerisRequest {
        zodiac_mode: ZodiacMode::Sidereal {
            ayanamsa: pleiades_types::Ayanamsa::FaganBradley,
        },
        frame: CoordinateFrame::Ecliptic,
        apparent: Apparentness::Mean,
        observer: None,
        ..frame_request.clone()
    };
    let error = validate_request_against_metadata(&sidereal_request, &metadata)
        .expect_err("sidereal requests should be rejected when metadata stays tropical-only");
    assert_eq!(error.kind, EphemerisErrorKind::UnsupportedZodiacMode);
    assert!(error.message.contains("tropical coordinates only"));

    let sidereal_invalid_observer_request = EphemerisRequest {
        observer: Some(ObserverLocation::new(
            Latitude::from_degrees(95.0),
            Longitude::from_degrees(-0.1),
            Some(45.0),
        )),
        ..sidereal_request.clone()
    };
    let error = validate_request_against_metadata(&sidereal_invalid_observer_request, &metadata)
        .expect_err("sidereal routing should win before malformed observer validation");
    assert_eq!(error.kind, EphemerisErrorKind::UnsupportedZodiacMode);
    assert!(error.message.contains("tropical coordinates only"));
    assert!(!error.message.contains("invalid observer location"));

    let unsupported_body_apparent_request = EphemerisRequest {
        body: CelestialBody::Mars,
        frame: CoordinateFrame::Ecliptic,
        apparent: Apparentness::Apparent,
        ..frame_request.clone()
    };
    let unsupported_body_apparent_error =
        validate_request_against_metadata(&unsupported_body_apparent_request, &metadata)
            .expect_err("apparentness should win before unsupported body coverage");
    assert_eq!(
        unsupported_body_apparent_error.kind,
        EphemerisErrorKind::UnsupportedApparentness
    );
    assert_eq!(unsupported_body_apparent_error.message, "toy backend currently returns mean geometric coordinates only; apparent corrections are not implemented");

    let unsupported_body_apparent_batch_error = validate_requests_against_metadata(
        &[
            geocentric_only_request.clone(),
            unsupported_body_apparent_request.clone(),
        ],
        &metadata,
    )
    .expect_err("batch validation should preserve apparentness precedence before body coverage");
    assert_eq!(
        unsupported_body_apparent_batch_error.kind,
        EphemerisErrorKind::UnsupportedApparentness
    );
    assert_eq!(unsupported_body_apparent_batch_error.message, "batch request 2: toy backend currently returns mean geometric coordinates only; apparent corrections are not implemented");

    let sidereal_metadata = BackendMetadata {
        capabilities: BackendCapabilities {
            native_sidereal: true,
            ..metadata.capabilities.clone()
        },
        ..metadata.clone()
    };
    assert!(validate_request_against_metadata(&sidereal_request, &sidereal_metadata).is_ok());

    let invalid_custom_body_request = EphemerisRequest {
        body: CelestialBody::Custom(pleiades_types::CustomBodyId::new("asteroid", " 433-Eros ")),
        ..frame_request.clone()
    };
    let invalid_custom_body_error = validate_request_against_metadata(
        &invalid_custom_body_request,
        &BackendMetadata {
            body_claims: vec![CelestialBody::Custom(pleiades_types::CustomBodyId::new(
                "asteroid",
                " 433-Eros ",
            ))
            .into()],
            ..metadata.clone()
        },
    )
    .expect_err("custom body identifiers should validate before metadata dispatch");
    assert_eq!(
        invalid_custom_body_error.kind,
        EphemerisErrorKind::InvalidRequest
    );
    assert!(invalid_custom_body_error
            .message
            .contains("request body is invalid: custom body id designation must not have leading or trailing whitespace"));

    let invalid_custom_ayanamsa_request = EphemerisRequest {
        zodiac_mode: ZodiacMode::Sidereal {
            ayanamsa: pleiades_types::Ayanamsa::Custom(pleiades_types::CustomAyanamsa {
                name: "  ".to_string(),
                description: Some("local calibration".to_string()),
                epoch: Some(pleiades_types::JulianDay::from_days(2451545.0)),
                offset_degrees: Some(pleiades_types::Angle::from_degrees(24.0)),
            }),
        },
        ..frame_request.clone()
    };
    let invalid_custom_ayanamsa_error =
        validate_request_against_metadata(&invalid_custom_ayanamsa_request, &sidereal_metadata)
            .expect_err("custom ayanamsas should validate before sidereal request dispatch");
    assert_eq!(
        invalid_custom_ayanamsa_error.kind,
        EphemerisErrorKind::InvalidRequest
    );
    assert!(invalid_custom_ayanamsa_error
        .message
        .contains("sidereal ayanamsa is invalid: custom ayanamsa name must not be blank"));

    let error = validate_zodiac_policy(&sidereal_request, "toy backend", &[ZodiacMode::Tropical])
        .expect_err("sidereal requests should be rejected when only tropical output is supported");
    assert_eq!(error.kind, EphemerisErrorKind::UnsupportedZodiacMode);
    assert!(error.message.contains("tropical coordinates only"));
    let request_policy = current_request_policy_summary();
    assert_eq!(
            request_policy.time_scale,
            "direct backend requests accept TT/TDB; civil UTC/UT1 inputs convert via the pleiades-time crate or caller-supplied offsets; the ephemeris backends carry no internal Delta T or UTC convenience model"
        );
    assert_eq!(
            request_policy.observer,
            "chart houses use observer locations; chart body observers stay separate; body requests stay geocentric; geocentric-only backends reject observer-bearing requests with UnsupportedObserver; malformed observer coordinates remain InvalidObserver; chart-layer topocentric body positions are supported as an opt-in correction (diurnal parallax + diurnal aberration); native-backend topocentric remains unsupported"
        );
    assert_eq!(
            request_policy.apparentness,
            "backends remain mean-only and J2000 at the backend boundary; apparent place of date (chart layer, default): light-time + precession-to-date + annual aberration + nutation-in-longitude, release-grade bodies; gravitational light-deflection omitted"
        );
    assert_eq!(
            request_policy.frame,
            "ecliptic body positions are the default request shape; at the backend boundary equatorial output is derived via mean-obliquity transforms when supported, while the chart layer reports apparent equatorial of date (true obliquity = mean obliquity + nutation-in-obliquity) for release-grade bodies; supported equatorial precision is bounded by the shared mean-obliquity frame round-trip envelope; native sidereal backend output remains unsupported unless a backend explicitly advertises it"
        );
    assert_eq!(
        time_scale_policy_summary_for_report().summary_line(),
        request_policy.time_scale
    );
    assert_eq!(
        observer_policy_summary_for_report().summary_line(),
        request_policy.observer
    );
    assert_eq!(
        apparentness_policy_summary_for_report().summary_line(),
        request_policy.apparentness
    );
    assert_eq!(frame_policy_summary_for_report(), request_policy.frame);
    assert_eq!(
        zodiac_policy_summary_for_report(&[ZodiacMode::Tropical]),
        "tropical only"
    );

    let observer_request = EphemerisRequest {
        observer: Some(ObserverLocation::new(
            Latitude::from_degrees(51.5),
            Longitude::from_degrees(-0.1),
            Some(45.0),
        )),
        ..apparent_request.clone()
    };
    let error = validate_observer_policy(&observer_request, "toy backend", false)
        .expect_err("topocentric requests should be rejected when unsupported");
    assert_eq!(error.kind, EphemerisErrorKind::UnsupportedObserver);
    assert!(error.message.contains("toy backend is geocentric only"));
    assert!(error
        .message
        .contains(&observer_request.observer.as_ref().unwrap().summary_line()));
}
