//! Property-based tests for BoundingBox using proptest.

use olga::model::BoundingBox;
use proptest::prelude::*;

/// Generate a valid normalized BoundingBox (all values in 0.0–1.0, within bounds).
fn arb_bbox() -> impl Strategy<Value = BoundingBox> {
    (0.0f32..=1.0, 0.0f32..=1.0).prop_flat_map(|(x, y)| {
        let max_w = (1.0 - x).max(f32::MIN_POSITIVE);
        let max_h = (1.0 - y).max(f32::MIN_POSITIVE);
        (Just(x), Just(y), 0.0..=max_w, 0.0..=max_h)
            .prop_map(|(x, y, w, h)| BoundingBox::new(x, y, w, h))
    })
}

proptest! {
    /// A bounding box generated by arb_bbox is always valid.
    #[test]
    fn bbox_arb_always_valid(bbox in arb_bbox()) {
        prop_assert!(bbox.is_valid());
    }

    /// overlap is symmetric.
    #[test]
    fn bbox_overlap_symmetric(a in arb_bbox(), b in arb_bbox()) {
        prop_assert_eq!(a.overlaps(&b), b.overlaps(&a));
    }

    /// A box always contains itself.
    #[test]
    fn bbox_contains_self(a in arb_bbox()) {
        prop_assert!(a.contains(&a));
    }

    /// merge is commutative.
    #[test]
    fn bbox_merge_commutative(a in arb_bbox(), b in arb_bbox()) {
        let m1 = a.merge(&b);
        let m2 = b.merge(&a);
        prop_assert!((m1.x - m2.x).abs() < 1e-5);
        prop_assert!((m1.y - m2.y).abs() < 1e-5);
        prop_assert!((m1.width - m2.width).abs() < 1e-5);
        prop_assert!((m1.height - m2.height).abs() < 1e-5);
    }

    /// merge(a, b) contains both a and b.
    #[test]
    fn bbox_merge_contains_both(a in arb_bbox(), b in arb_bbox()) {
        let m = a.merge(&b);
        // Allow for floating point tolerance
        prop_assert!(m.x <= a.x + 1e-5);
        prop_assert!(m.y <= a.y + 1e-5);
        prop_assert!(m.right() >= a.right() - 1e-5);
        prop_assert!(m.bottom() >= a.bottom() - 1e-5);
        prop_assert!(m.x <= b.x + 1e-5);
        prop_assert!(m.y <= b.y + 1e-5);
        prop_assert!(m.right() >= b.right() - 1e-5);
        prop_assert!(m.bottom() >= b.bottom() - 1e-5);
    }

    /// distance is symmetric.
    #[test]
    fn bbox_distance_symmetric(a in arb_bbox(), b in arb_bbox()) {
        prop_assert!((a.distance(&b) - b.distance(&a)).abs() < 1e-5);
    }

    /// distance is non-negative.
    #[test]
    fn bbox_distance_non_negative(a in arb_bbox(), b in arb_bbox()) {
        prop_assert!(a.distance(&b) >= -1e-6);
    }

    /// If a.overlaps(b) then distance(a, b) == 0.
    #[test]
    fn bbox_overlapping_distance_zero(a in arb_bbox(), b in arb_bbox()) {
        if a.overlaps(&b) {
            prop_assert!(a.distance(&b).abs() < 1e-5);
        }
    }

    /// intersection is symmetric (both None or both equal).
    #[test]
    fn bbox_intersection_symmetric(a in arb_bbox(), b in arb_bbox()) {
        let i1 = a.intersection(&b);
        let i2 = b.intersection(&a);
        match (i1, i2) {
            (None, None) => {},
            (Some(r1), Some(r2)) => {
                prop_assert!((r1.x - r2.x).abs() < 1e-5);
                prop_assert!((r1.y - r2.y).abs() < 1e-5);
                prop_assert!((r1.width - r2.width).abs() < 1e-5);
                prop_assert!((r1.height - r2.height).abs() < 1e-5);
            }
            _ => prop_assert!(false, "intersection symmetry violated"),
        }
    }

    /// The center of a bbox is always contained in it (for non-zero-area boxes).
    #[test]
    fn bbox_center_is_contained(bbox in arb_bbox()) {
        if bbox.width > 0.0 && bbox.height > 0.0 {
            prop_assert!(bbox.contains_point(&bbox.center()));
        }
    }

    /// Serialization round-trip preserves all fields.
    #[test]
    fn bbox_serde_roundtrip(bbox in arb_bbox()) {
        let json = serde_json::to_string(&bbox).unwrap();
        let bbox2: BoundingBox = serde_json::from_str(&json).unwrap();
        prop_assert!((bbox.x - bbox2.x).abs() < 1e-6);
        prop_assert!((bbox.y - bbox2.y).abs() < 1e-6);
        prop_assert!((bbox.width - bbox2.width).abs() < 1e-6);
        prop_assert!((bbox.height - bbox2.height).abs() < 1e-6);
    }
}
