use std::time::Duration;

use super::super::types::NoopWalWriter;
use super::*;
use crate::schema::{DecaySpec, EntityKind, SchemaBuilder, Window};

fn test_schema() -> Schema {
    let mut builder = SchemaBuilder::new();
    let _ = builder
        .signal(
            "view",
            EntityKind::Item,
            DecaySpec::Exponential {
                half_life: Duration::from_secs(7 * 24 * 3600),
            },
        )
        .windows(&[
            Window::OneHour,
            Window::TwentyFourHours,
            Window::SevenDays,
            Window::AllTime,
        ])
        .velocity(true)
        .add();
    builder.build().expect("valid test schema")
}

fn test_schema_multi() -> Schema {
    let mut builder = SchemaBuilder::new();
    let _ = builder
        .signal(
            "like",
            EntityKind::Item,
            DecaySpec::Exponential {
                half_life: Duration::from_secs(14 * 24 * 3600),
            },
        )
        .windows(&[Window::AllTime])
        .velocity(false)
        .add();
    let _ = builder
        .signal(
            "skip",
            EntityKind::Item,
            DecaySpec::Exponential {
                half_life: Duration::from_secs(24 * 3600),
            },
        )
        .windows(&[Window::AllTime])
        .velocity(false)
        .add();
    let _ = builder
        .signal(
            "view",
            EntityKind::Item,
            DecaySpec::Exponential {
                half_life: Duration::from_secs(7 * 24 * 3600),
            },
        )
        .windows(&[Window::AllTime])
        .velocity(false)
        .add();
    builder.build().expect("valid multi-signal test schema")
}

#[test]
fn ledger_record_and_read() {
    let schema = test_schema();
    let ledger = SignalLedger::new(schema, Box::new(NoopWalWriter));
    let entity_id = EntityId::new(42);

    let now = Timestamp::now();
    ledger
        .record_signal("view", entity_id, 1.0, now)
        .expect("record");

    let score = ledger
        .read_decay_score(entity_id, "view", 0)
        .expect("score");
    assert!(score.is_some());
    assert!(score.expect("some") > 0.0);

    let count = ledger
        .read_windowed_count(entity_id, "view", Window::OneHour)
        .expect("count");
    assert_eq!(count, 1);

    let all_time = ledger
        .read_windowed_count(entity_id, "view", Window::AllTime)
        .expect("all_time");
    assert_eq!(all_time, 1);
}

#[test]
fn ledger_unknown_signal_type_returns_error() {
    let schema = test_schema();
    let ledger = SignalLedger::new(schema, Box::new(NoopWalWriter));

    let result = ledger.record_signal("nonexistent", EntityId::new(1), 1.0, Timestamp::now());
    assert!(result.is_err());
}

#[test]
fn ledger_read_nonexistent_entity_returns_none() {
    let schema = test_schema();
    let ledger = SignalLedger::new(schema, Box::new(NoopWalWriter));

    let score = ledger
        .read_decay_score(EntityId::new(999), "view", 0)
        .expect("ok");
    assert!(score.is_none());

    let count = ledger
        .read_windowed_count(EntityId::new(999), "view", Window::OneHour)
        .expect("ok");
    assert_eq!(count, 0);

    let velocity = ledger
        .read_velocity(EntityId::new(999), "view", Window::OneHour)
        .expect("ok");
    assert!((velocity - 0.0).abs() < 1e-15);
}

#[test]
fn ledger_velocity_all_time_is_zero() {
    let schema = test_schema();
    let ledger = SignalLedger::new(schema, Box::new(NoopWalWriter));
    let entity_id = EntityId::new(1);

    ledger
        .record_signal("view", entity_id, 1.0, Timestamp::now())
        .expect("record");
    let velocity = ledger
        .read_velocity(entity_id, "view", Window::AllTime)
        .expect("ok");
    assert!(
        (velocity - 0.0).abs() < 1e-15,
        "all-time velocity should be 0.0"
    );
}

#[test]
fn ledger_multiple_signal_types() {
    let schema = test_schema_multi();
    let ledger = SignalLedger::new(schema, Box::new(NoopWalWriter));
    let entity_id = EntityId::new(1);
    let now = Timestamp::now();

    ledger
        .record_signal("view", entity_id, 1.0, now)
        .expect("record view");
    ledger
        .record_signal("like", entity_id, 1.0, now)
        .expect("record like");

    let view_count = ledger
        .read_windowed_count(entity_id, "view", Window::AllTime)
        .expect("ok");
    let like_count = ledger
        .read_windowed_count(entity_id, "like", Window::AllTime)
        .expect("ok");
    let skip_count = ledger
        .read_windowed_count(entity_id, "skip", Window::AllTime)
        .expect("ok");

    assert_eq!(view_count, 1);
    assert_eq!(like_count, 1);
    assert_eq!(skip_count, 0);
}

#[test]
fn ledger_multiple_entities() {
    let schema = test_schema();
    let ledger = SignalLedger::new(schema, Box::new(NoopWalWriter));
    let now = Timestamp::now();

    ledger
        .record_signal("view", EntityId::new(1), 1.0, now)
        .expect("1");
    ledger
        .record_signal("view", EntityId::new(2), 1.0, now)
        .expect("2a");
    ledger
        .record_signal("view", EntityId::new(2), 1.0, now)
        .expect("2b");

    let count1 = ledger
        .read_windowed_count(EntityId::new(1), "view", Window::AllTime)
        .expect("ok");
    let count2 = ledger
        .read_windowed_count(EntityId::new(2), "view", Window::AllTime)
        .expect("ok");

    assert_eq!(count1, 1);
    assert_eq!(count2, 2);
}

#[test]
fn ledger_is_send_and_sync() {
    fn assert_send_sync<T: Send + Sync>() {}
    assert_send_sync::<SignalLedger>();
}

#[test]
fn signal_type_id_newtype() {
    use super::super::super::SignalTypeId;
    let id = SignalTypeId::new(5);
    assert_eq!(id.as_u16(), 5);
    assert_eq!(id.to_string(), "5");
    assert_eq!(id, SignalTypeId::new(5));
    assert_ne!(id, SignalTypeId::new(6));
}

#[allow(clippy::cast_precision_loss)]
mod proptests {
    use std::time::Duration;

    use proptest::prelude::*;

    use super::super::super::types::NoopWalWriter;
    use super::super::*;
    use crate::schema::{DecaySpec, EntityKind, SchemaBuilder, Window};

    fn test_schema() -> Schema {
        let mut builder = SchemaBuilder::new();
        let _ = builder
            .signal(
                "view",
                EntityKind::Item,
                DecaySpec::Exponential {
                    half_life: Duration::from_secs(7 * 24 * 3600),
                },
            )
            .windows(&[Window::AllTime])
            .velocity(true)
            .add();
        builder.build().expect("valid schema")
    }

    // Velocity equals windowed_count / duration for all windows.
    proptest! {
        #[test]
        fn velocity_equals_count_over_duration(
            event_count in 1u64..1000,
        ) {
            let schema = test_schema();
            let ledger = SignalLedger::new(schema, Box::new(NoopWalWriter));
            let entity_id = EntityId::new(1);

            // All events in the current minute (within 1h window).
            let now = Timestamp::now();
            for i in 0..event_count {
                let ts = Timestamp::from_nanos(now.as_nanos() + i * 1_000_000);
                ledger.record_signal("view", entity_id, 1.0, ts).expect("record");
            }

            let count_1h = ledger
                .read_windowed_count(entity_id, "view", Window::OneHour)
                .expect("ok");
            let velocity_1h = ledger
                .read_velocity(entity_id, "view", Window::OneHour)
                .expect("ok");

            let expected_velocity = count_1h as f64 / Window::OneHour.duration_secs_f64();
            prop_assert!(
                (velocity_1h - expected_velocity).abs() < 1e-15,
                "velocity={velocity_1h}, expected={expected_velocity}"
            );
        }
    }
}
