use super::*;

#[test]
fn hot_signal_state_size_and_alignment() {
    assert_eq!(std::mem::size_of::<HotSignalState>(), 64);
    assert_eq!(std::mem::align_of::<HotSignalState>(), 64);
}

#[test]
fn new_state_is_zeroed() {
    let state = HotSignalState::new(42, 5);
    assert_eq!(state.entity_id(), 42);
    assert_eq!(state.signal_type_id(), 5);
    assert_eq!(state.last_update_ns(), 0);
    assert_eq!(state.stored_score(0), 0.0);
    assert_eq!(state.stored_score(1), 0.0);
    assert_eq!(state.stored_score(2), 0.0);
}

#[test]
fn single_event_sets_score_to_weight() {
    let state = HotSignalState::new(1, 0);
    let lambda = std::f64::consts::LN_2 / (7.0 * 24.0 * 3600.0); // 7-day half-life
    let t = 1_000_000_000u64; // 1 second in nanos

    state.on_signal(1.0, t, &[lambda]);

    // Immediately after, with no time elapsed, score should be ~1.0
    let score = state.current_score(0, t, lambda);
    assert!((score - 1.0).abs() < 1e-10);
}

#[test]
fn score_halves_after_half_life() {
    let half_life_secs = 3600.0; // 1 hour
    let lambda = std::f64::consts::LN_2 / half_life_secs;
    let state = HotSignalState::new(1, 0);

    let t0 = 0u64;
    state.on_signal(1.0, t0, &[lambda]);

    // Read after exactly one half-life
    let t1 = (half_life_secs * 1e9) as u64;
    let score = state.current_score(0, t1, lambda);
    assert!(
        (score - 0.5).abs() < 1e-10,
        "score was {score}, expected ~0.5"
    );
}

#[test]
fn two_events_accumulate() {
    let lambda = std::f64::consts::LN_2 / 3600.0; // 1h half-life
    let state = HotSignalState::new(1, 0);

    let t0 = 0u64;
    let t1 = 1_000_000_000u64; // 1 second later

    state.on_signal(1.0, t0, &[lambda]);
    state.on_signal(1.0, t1, &[lambda]);

    let score = state.current_score(0, t1, lambda);
    // score = 1.0 * exp(-lambda * 1.0) + 1.0
    let expected = 1.0_f64.mul_add((-lambda * 1.0).exp(), 1.0);
    assert!(
        (score - expected).abs() < 1e-10,
        "score={score}, expected={expected}"
    );
}

#[test]
fn out_of_order_event_predecays_weight() {
    let lambda = std::f64::consts::LN_2 / 3600.0;
    let state = HotSignalState::new(1, 0);

    // Process event at t=10s first
    let t_late = 10_000_000_000u64;
    state.on_signal(1.0, t_late, &[lambda]);

    // Then process event at t=5s (out of order)
    let t_early = 5_000_000_000u64;
    state.on_signal(1.0, t_early, &[lambda]);

    // Query at t=10s -- should match analytical result
    let analytical = 1.0f64.mul_add((-lambda * 0.0).exp(), 1.0 * (-lambda * 5.0).exp());
    let actual = state.current_score(0, t_late, lambda);
    assert!(
        (actual - analytical).abs() < 1e-10,
        "actual={actual}, analytical={analytical}"
    );
}

#[test]
fn last_update_ns_not_regressed_by_out_of_order() {
    let lambda = std::f64::consts::LN_2 / 3600.0;
    let state = HotSignalState::new(1, 0);

    state.on_signal(1.0, 10_000_000_000, &[lambda]);
    let ts_before = state.last_update_ns();

    state.on_signal(1.0, 5_000_000_000, &[lambda]); // older event
    let ts_after = state.last_update_ns();

    assert_eq!(ts_before, ts_after, "timestamp should not regress");
    assert_eq!(ts_after, 10_000_000_000);
}

#[test]
fn score_decays_to_near_zero_after_many_half_lives() {
    let lambda = std::f64::consts::LN_2 / 3600.0; // 1h half-life
    let state = HotSignalState::new(1, 0);

    state.on_signal(1.0, 0, &[lambda]);

    // After 100 half-lives (~100 hours), score should be essentially zero
    let t = (100.0 * 3600.0 * 1e9) as u64;
    let score = state.current_score(0, t, lambda);
    assert!(score < 1e-20, "score was {score}");
}

#[test]
fn velocity_flag() {
    let state = HotSignalState::with_flags(1, 0, true);
    assert!(state.velocity_enabled());

    let state2 = HotSignalState::with_flags(1, 0, false);
    assert!(!state2.velocity_enabled());
}

#[test]
fn restore_sets_all_fields() {
    let state = HotSignalState::new(1, 0);
    state.restore(42_000_000_000, &[1.5, 2.5, 3.5]);

    assert_eq!(state.last_update_ns(), 42_000_000_000);
    assert!((state.stored_score(0) - 1.5).abs() < 1e-15);
    assert!((state.stored_score(1) - 2.5).abs() < 1e-15);
    assert!((state.stored_score(2) - 3.5).abs() < 1e-15);
}

#[test]
fn multiple_lambdas() {
    let lambda_fast = std::f64::consts::LN_2 / 3600.0; // 1h half-life
    let lambda_slow = std::f64::consts::LN_2 / 604_800.0; // 7d half-life
    let lambdas = [lambda_fast, lambda_slow];
    let state = HotSignalState::new(1, 0);

    state.on_signal(1.0, 0, &lambdas);

    // After 1 hour, fast score ~0.5, slow score ~0.9996
    let t = (3600.0 * 1e9) as u64;
    let score_fast = state.current_score(0, t, lambda_fast);
    let score_slow = state.current_score(1, t, lambda_slow);
    assert!((score_fast - 0.5).abs() < 1e-6);
    assert!((score_slow - (-lambda_slow * 3600.0).exp()).abs() < 1e-6);
    assert!(score_slow > score_fast, "slow decay should retain more");
}

mod proptests {
    use super::*;
    use proptest::prelude::*;

    // P1: Decay scores decrease monotonically without new events.
    proptest! {
        #[test]
        fn decay_monotonic_decrease(
            initial_score in 0.0f64..1e12,
            lambda in 1e-7f64..1e-3,
            dt_secs in 1.0f64..1e7,
        ) {
            let decayed = initial_score * (-lambda * dt_secs).exp();
            prop_assert!(decayed <= initial_score);
            prop_assert!(decayed >= 0.0);
        }
    }

    // P2: Running score matches analytical sum to 6 decimal places.
    proptest! {
        #[test]
        fn running_score_matches_analytical(
            events in proptest::collection::vec(
                (0.1f64..10.0, 1_000_000u64..1_000_000_000),
                1..100,
            ),
            lambda in 1e-7f64..1e-3,
        ) {
            // Sort events by time for in-order processing
            let mut sorted_events = events;
            sorted_events.sort_by_key(|e| e.1);

            let query_time_ns = sorted_events.last().expect("events non-empty").1 + 1_000_000_000; // +1 second

            // Build HotSignalState and process events
            let state = HotSignalState::new(42, 0);
            for &(weight, time_ns) in &sorted_events {
                state.on_signal(weight, time_ns, &[lambda]);
            }
            let running = state.current_score(0, query_time_ns, lambda);

            // Compute analytical sum
            let analytical: f64 = sorted_events.iter()
                .map(|&(w, t)| w * (-lambda * (query_time_ns - t) as f64 / 1e9).exp())
                .sum();

            let relative_error = if analytical.abs() < 1e-15 {
                running.abs()
            } else {
                (running - analytical).abs() / analytical
            };
            prop_assert!(
                relative_error < 1e-6,
                "running={running}, analytical={analytical}, relative_error={relative_error}"
            );
        }
    }

    // P4: Out-of-order events produce same final score as in-order.
    proptest! {
        #[test]
        fn out_of_order_events_commutative(
            events in proptest::collection::vec(
                (0.1f64..10.0, 1_000_000u64..1_000_000_000),
                2..50,
            ),
            lambda in 1e-7f64..1e-3,
        ) {
            let query_time_ns = events.iter().map(|e| e.1).max().expect("events non-empty") + 1_000_000_000;

            // Process in-order
            let mut sorted = events.clone();
            sorted.sort_by_key(|e| e.1);
            let state_ordered = HotSignalState::new(42, 0);
            for &(w, t) in &sorted {
                state_ordered.on_signal(w, t, &[lambda]);
            }
            let score_ordered = state_ordered.current_score(0, query_time_ns, lambda);

            // Process in reverse order (all out-of-order except first)
            sorted.reverse();
            let state_reversed = HotSignalState::new(42, 0);
            for &(w, t) in &sorted {
                state_reversed.on_signal(w, t, &[lambda]);
            }
            let score_reversed = state_reversed.current_score(0, query_time_ns, lambda);

            // Also compare to analytical sum
            let analytical: f64 = events.iter()
                .map(|&(w, t)| w * (-lambda * (query_time_ns - t) as f64 / 1e9).exp())
                .sum();

            let error_ordered = if analytical.abs() < 1e-15 {
                score_ordered.abs()
            } else {
                (score_ordered - analytical).abs() / analytical
            };
            let error_reversed = if analytical.abs() < 1e-15 {
                score_reversed.abs()
            } else {
                (score_reversed - analytical).abs() / analytical
            };

            prop_assert!(error_ordered < 1e-6,
                "ordered: running={score_ordered}, analytical={analytical}, error={error_ordered}");
            prop_assert!(error_reversed < 1e-6,
                "reversed: running={score_reversed}, analytical={analytical}, error={error_reversed}");
        }
    }

    // Decay scores are always non-negative (INV-SIG-3).
    proptest! {
        #[test]
        fn decay_scores_non_negative(
            events in proptest::collection::vec(
                (0.0f64..100.0, 0u64..2_000_000_000),
                1..200,
            ),
            lambda in 1e-7f64..1e-3,
            query_offset in 0u64..2_000_000_000,
        ) {
            let state = HotSignalState::new(1, 0);
            for &(w, t) in &events {
                state.on_signal(w, t, &[lambda]);
            }
            let query_time = events.iter().map(|e| e.1).max().unwrap_or(0) + query_offset;
            let score = state.current_score(0, query_time, lambda);
            prop_assert!(score >= 0.0, "score was {score}");
        }
    }
}
