//! UAT: Milestone 1, Phase 1 — Core Type System and Schema
//!
//! Exercises every m1p1 acceptance criterion from the public API.
//! Does NOT duplicate the unit tests in `schema/entity.rs`, `schema/signal.rs`,
//! `schema/error.rs`, `schema/score.rs`, or `schema/validation.rs`.
//! Instead it verifies from the *user* perspective: import `tidaldb::schema::*`,
//! construct types, and assert the documented guarantees hold.

#![allow(clippy::unwrap_used, unused_must_use)]

use std::collections::HashSet;
use std::time::Duration;

use tidaldb::schema::{
    DecaySpec, EntityId, EntityKind, SchemaBuilder, SchemaError, Score, Window, WindowSet,
};

// ── UAT-01: EntityId — u64 newtype with Display, Hash, Eq, Ord, to_be_bytes ──

#[test]
fn uat01_entity_id_display() {
    let id = EntityId::new(42);
    assert_eq!(id.to_string(), "42");
}

#[test]
fn uat01_entity_id_hash_eq() {
    let a = EntityId::new(7);
    let b = EntityId::new(7);
    let c = EntityId::new(8);

    // Eq
    assert_eq!(a, b);
    assert_ne!(a, c);

    // Hash: equal entities produce equal hashes (insert into HashSet, verify dedup)
    let mut set = HashSet::new();
    set.insert(a);
    set.insert(b);
    set.insert(c);
    assert_eq!(set.len(), 2, "HashSet should deduplicate equal EntityIds");
}

#[test]
fn uat01_entity_id_ord() {
    let ids: Vec<EntityId> = vec![
        EntityId::new(100),
        EntityId::new(1),
        EntityId::new(50),
        EntityId::new(0),
        EntityId::new(u64::MAX),
    ];
    let mut sorted = ids.clone();
    sorted.sort();
    let vals: Vec<u64> = sorted.iter().map(|id| id.as_u64()).collect();
    assert_eq!(vals, vec![0, 1, 50, 100, u64::MAX]);
}

#[test]
fn uat01_entity_id_be_bytes_preserves_ordering() {
    // The acceptance criterion: big-endian encoding preserves numeric ordering.
    let pairs = [
        (0_u64, 1_u64),
        (1, 2),
        (255, 256),
        (u64::MAX - 1, u64::MAX),
        (0, u64::MAX),
    ];
    for (a, b) in pairs {
        let bytes_a = EntityId::new(a).to_be_bytes();
        let bytes_b = EntityId::new(b).to_be_bytes();
        assert!(
            bytes_a < bytes_b,
            "be_bytes ordering violated: {a} < {b} but bytes {bytes_a:?} >= {bytes_b:?}"
        );
    }
}

// ── UAT-02: EntityKind — Item, User, Creator ─────────────────────────────────

#[test]
fn uat02_entity_kind_variants_exist() {
    let _item = EntityKind::Item;
    let _user = EntityKind::User;
    let _creator = EntityKind::Creator;
}

#[test]
fn uat02_entity_kind_display() {
    assert_eq!(EntityKind::Item.to_string(), "item");
    assert_eq!(EntityKind::User.to_string(), "user");
    assert_eq!(EntityKind::Creator.to_string(), "creator");
}

// ── UAT-03: SignalTypeDef via SchemaBuilder ──────────────────────────────────

#[test]
fn uat03_signal_type_def_captures_all_fields() {
    let mut builder = SchemaBuilder::new();
    builder
        .signal(
            "view",
            EntityKind::Item,
            DecaySpec::Exponential {
                half_life: Duration::from_secs(604_800),
            },
        )
        .windows(&[Window::OneHour, Window::TwentyFourHours])
        .velocity(true)
        .add();

    let schema = builder.build().unwrap();
    let view = schema.signal("view").unwrap();

    assert_eq!(view.name(), "view");
    assert_eq!(view.target(), EntityKind::Item);
    assert!(view.velocity_enabled());
    assert_eq!(view.windows().len(), 2);
    assert!(view.windows().contains(&Window::OneHour));
    assert!(view.windows().contains(&Window::TwentyFourHours));
    assert!(view.decay().lambda().is_some());
    assert!(view.decay().half_life().is_some());
}

#[test]
fn uat03_signal_type_def_user_target() {
    let mut builder = SchemaBuilder::new();
    builder
        .signal("follow", EntityKind::User, DecaySpec::Permanent)
        .add();
    let schema = builder.build().unwrap();
    let follow = schema.signal("follow").unwrap();
    assert_eq!(follow.target(), EntityKind::User);
}

#[test]
fn uat03_signal_type_def_creator_target() {
    let mut builder = SchemaBuilder::new();
    builder
        .signal("subscribe", EntityKind::Creator, DecaySpec::Permanent)
        .add();
    let schema = builder.build().unwrap();
    let sub = schema.signal("subscribe").unwrap();
    assert_eq!(sub.target(), EntityKind::Creator);
}

// ── UAT-04: DecayModel — pre-computed lambda, no division on hot path ────────

#[test]
fn uat04_exponential_decay_precomputes_lambda() {
    let half_life_secs = 7.0 * 24.0 * 3600.0; // 7 days
    let expected_lambda = std::f64::consts::LN_2 / half_life_secs;

    let mut builder = SchemaBuilder::new();
    builder
        .signal(
            "view",
            EntityKind::Item,
            DecaySpec::Exponential {
                half_life: Duration::from_secs(604_800),
            },
        )
        .windows(&[Window::AllTime])
        .add();
    let schema = builder.build().unwrap();
    let view = schema.signal("view").unwrap();

    let lambda = view.decay().lambda().unwrap();
    assert!(
        (lambda - expected_lambda).abs() < 1e-15,
        "lambda should be precomputed as ln(2)/half_life: got {lambda}, expected {expected_lambda}"
    );
}

#[test]
fn uat04_linear_decay_has_no_lambda() {
    let mut builder = SchemaBuilder::new();
    builder
        .signal(
            "impression",
            EntityKind::Item,
            DecaySpec::Linear {
                lifetime: Duration::from_secs(86_400),
            },
        )
        .windows(&[Window::TwentyFourHours])
        .add();
    let schema = builder.build().unwrap();
    let sig = schema.signal("impression").unwrap();
    assert!(sig.decay().lambda().is_none());
    assert!(sig.decay().half_life().is_none());
}

#[test]
fn uat04_permanent_decay_has_no_lambda() {
    let mut builder = SchemaBuilder::new();
    builder
        .signal("block", EntityKind::User, DecaySpec::Permanent)
        .add();
    let schema = builder.build().unwrap();
    let sig = schema.signal("block").unwrap();
    assert!(sig.decay().lambda().is_none());
    assert!(sig.decay().half_life().is_none());
}

// ── UAT-05: Window enum — all variants, duration(), label(), duration_secs_f64() ─

#[test]
fn uat05_window_variants_and_methods() {
    let windows = [
        (Window::OneHour, Duration::from_secs(3_600), "1h", 3_600.0),
        (
            Window::TwentyFourHours,
            Duration::from_secs(86_400),
            "24h",
            86_400.0,
        ),
        (
            Window::SevenDays,
            Duration::from_secs(604_800),
            "7d",
            604_800.0,
        ),
        (
            Window::ThirtyDays,
            Duration::from_secs(2_592_000),
            "30d",
            2_592_000.0,
        ),
    ];

    for (window, expected_dur, expected_label, expected_secs) in windows {
        assert_eq!(
            window.duration(),
            expected_dur,
            "duration mismatch for {expected_label}"
        );
        assert_eq!(window.label(), expected_label);
        assert!(
            (window.duration_secs_f64() - expected_secs).abs() < 1e-10,
            "duration_secs_f64 mismatch for {expected_label}"
        );
    }

    // AllTime is special
    assert_eq!(Window::AllTime.duration(), Duration::MAX);
    assert_eq!(Window::AllTime.label(), "all");
    assert!(Window::AllTime.duration_secs_f64().is_infinite());
}

// ── UAT-06: WindowSet — deduplicates and sorts; empty() for permanent ────────

#[test]
fn uat06_window_set_deduplicates_and_sorts() {
    let ws = WindowSet::new(&[
        Window::AllTime,
        Window::OneHour,
        Window::SevenDays,
        Window::OneHour, // duplicate
        Window::AllTime, // duplicate
    ]);
    assert_eq!(ws.len(), 3);

    let windows: Vec<Window> = ws.iter().copied().collect();
    assert_eq!(
        windows,
        vec![Window::OneHour, Window::SevenDays, Window::AllTime]
    );
}

#[test]
fn uat06_window_set_empty() {
    let ws = WindowSet::empty();
    assert!(ws.is_empty());
    assert_eq!(ws.len(), 0);
}

// ── UAT-07: Error types — all TidalError variants ───────────────────────────

#[test]
fn uat07_tidal_error_variants_exist() {
    use tidaldb::TidalError;

    // Verify the variants exist and implement Display (via thiserror).
    let errors: Vec<Box<dyn std::error::Error>> = vec![
        Box::new(TidalError::internal("test", "test")),
        Box::new(TidalError::NotFound {
            kind: EntityKind::Item,
            id: EntityId::new(1),
        }),
        Box::new(TidalError::Schema(SchemaError::NoSignalsDefined)),
    ];

    for e in &errors {
        // Display works
        let msg = e.to_string();
        assert!(!msg.is_empty());
    }
}

#[test]
fn uat07_schema_error_converts_to_tidal_error() {
    use tidaldb::TidalError;

    let schema_err = SchemaError::DuplicateSignalName("view".into());
    let tidal_err: TidalError = schema_err.into();
    assert!(matches!(
        tidal_err,
        TidalError::Schema(SchemaError::DuplicateSignalName(_))
    ));
}

// ── UAT-08: SchemaError — all validation error conditions ───────────────────

#[test]
fn uat08_rejects_duplicate_signal_names() {
    let mut builder = SchemaBuilder::new();
    builder
        .signal("view", EntityKind::Item, DecaySpec::Permanent)
        .add();
    builder
        .signal("view", EntityKind::Item, DecaySpec::Permanent)
        .add();
    let err = builder.build().unwrap_err();
    assert!(
        matches!(err, SchemaError::DuplicateSignalName(ref name) if name == "view"),
        "expected DuplicateSignalName, got: {err}"
    );
}

#[test]
fn uat08_rejects_invalid_identifiers() {
    let invalid_names = [
        "",
        "View",
        "1view",
        "view count",
        "view-count",
        "_view",
        "view!",
    ];
    for name in invalid_names {
        let mut builder = SchemaBuilder::new();
        builder
            .signal(name, EntityKind::Item, DecaySpec::Permanent)
            .add();
        let err = builder.build().unwrap_err();
        assert!(
            matches!(err, SchemaError::InvalidSignalName(_)),
            "expected InvalidSignalName for '{name}', got: {err}"
        );
    }
}

#[test]
fn uat08_rejects_zero_half_life() {
    let mut builder = SchemaBuilder::new();
    builder
        .signal(
            "bad",
            EntityKind::Item,
            DecaySpec::Exponential {
                half_life: Duration::ZERO,
            },
        )
        .windows(&[Window::AllTime])
        .add();
    let err = builder.build().unwrap_err();
    assert!(
        matches!(err, SchemaError::InvalidHalfLife { .. }),
        "expected InvalidHalfLife, got: {err}"
    );
}

#[test]
fn uat08_rejects_zero_lifetime() {
    let mut builder = SchemaBuilder::new();
    builder
        .signal(
            "bad",
            EntityKind::Item,
            DecaySpec::Linear {
                lifetime: Duration::ZERO,
            },
        )
        .windows(&[Window::AllTime])
        .add();
    let err = builder.build().unwrap_err();
    assert!(
        matches!(err, SchemaError::InvalidLifetime { .. }),
        "expected InvalidLifetime, got: {err}"
    );
}

#[test]
fn uat08_rejects_empty_windows_for_non_permanent() {
    // Exponential without windows
    let mut builder = SchemaBuilder::new();
    builder
        .signal(
            "bad",
            EntityKind::Item,
            DecaySpec::Exponential {
                half_life: Duration::from_secs(3600),
            },
        )
        .add();
    let err = builder.build().unwrap_err();
    assert!(
        matches!(err, SchemaError::EmptyWindows { .. }),
        "expected EmptyWindows for exponential, got: {err}"
    );

    // Linear without windows
    let mut builder = SchemaBuilder::new();
    builder
        .signal(
            "bad",
            EntityKind::Item,
            DecaySpec::Linear {
                lifetime: Duration::from_secs(3600),
            },
        )
        .add();
    let err = builder.build().unwrap_err();
    assert!(
        matches!(err, SchemaError::EmptyWindows { .. }),
        "expected EmptyWindows for linear, got: {err}"
    );
}

#[test]
fn uat08_accepts_empty_windows_for_permanent() {
    let mut builder = SchemaBuilder::new();
    builder
        .signal("hide", EntityKind::Item, DecaySpec::Permanent)
        .add();
    assert!(builder.build().is_ok());
}

#[test]
fn uat08_rejects_velocity_without_windows() {
    let mut builder = SchemaBuilder::new();
    builder
        .signal("bad", EntityKind::Item, DecaySpec::Permanent)
        .velocity(true)
        .add();
    let err = builder.build().unwrap_err();
    assert!(
        matches!(err, SchemaError::VelocityWithoutWindows { .. }),
        "expected VelocityWithoutWindows, got: {err}"
    );
}

#[test]
fn uat08_rejects_empty_schema() {
    let err = SchemaBuilder::new().build().unwrap_err();
    assert!(
        matches!(err, SchemaError::NoSignalsDefined),
        "expected NoSignalsDefined, got: {err}"
    );
}

// ── UAT-09: SchemaBuilder — valid multi-signal schema ───────────────────────

#[test]
fn uat09_valid_multi_signal_schema() {
    let mut builder = SchemaBuilder::new();

    // Exponential decay with velocity
    builder
        .signal(
            "view",
            EntityKind::Item,
            DecaySpec::Exponential {
                half_life: Duration::from_secs(7 * 24 * 3600),
            },
        )
        .windows(&[
            Window::OneHour,
            Window::TwentyFourHours,
            Window::SevenDays,
            Window::ThirtyDays,
            Window::AllTime,
        ])
        .velocity(true)
        .add();

    // Linear decay
    builder
        .signal(
            "impression",
            EntityKind::Item,
            DecaySpec::Linear {
                lifetime: Duration::from_secs(86_400),
            },
        )
        .windows(&[Window::TwentyFourHours])
        .velocity(false)
        .add();

    // Permanent
    builder
        .signal("hide", EntityKind::Item, DecaySpec::Permanent)
        .add();

    // User signal
    builder
        .signal("follow", EntityKind::User, DecaySpec::Permanent)
        .add();

    // Creator signal
    builder
        .signal("subscribe", EntityKind::Creator, DecaySpec::Permanent)
        .add();

    let schema = builder.build().unwrap();
    assert_eq!(schema.signal_count(), 5);

    // Verify all signals are retrievable
    assert!(schema.signal("view").is_some());
    assert!(schema.signal("impression").is_some());
    assert!(schema.signal("hide").is_some());
    assert!(schema.signal("follow").is_some());
    assert!(schema.signal("subscribe").is_some());
    assert!(schema.signal("nonexistent").is_none());

    // Verify iterate signals returns all
    let names: HashSet<&str> = schema.signals().map(|s| s.name()).collect();
    assert_eq!(names.len(), 5);
    assert!(names.contains("view"));
    assert!(names.contains("impression"));
    assert!(names.contains("hide"));
    assert!(names.contains("follow"));
    assert!(names.contains("subscribe"));
}

// ── UAT-10: Score — rejects NaN/Inf, accepts finite, total ordering ─────────

#[test]
fn uat10_score_rejects_nan_and_inf() {
    assert!(Score::new(f64::NAN).is_none());
    assert!(Score::new(f64::INFINITY).is_none());
    assert!(Score::new(f64::NEG_INFINITY).is_none());
}

#[test]
fn uat10_score_accepts_finite_values() {
    let s = Score::new(0.75).unwrap();
    assert!((s.as_f64() - 0.75).abs() < 1e-15);

    let neg = Score::new(-10.0).unwrap();
    assert!((neg.as_f64() - (-10.0)).abs() < 1e-15);

    assert_eq!(Score::ZERO.as_f64(), 0.0);
}

#[test]
fn uat10_score_total_ordering() {
    let a = Score::new(-1.0).unwrap();
    let b = Score::ZERO;
    let c = Score::new(0.5).unwrap();
    let d = Score::new(1.0).unwrap();

    assert!(a < b);
    assert!(b < c);
    assert!(c < d);
    assert_eq!(b, Score::new(0.0).unwrap());

    // Sort works
    let mut scores = vec![d, a, c, b];
    scores.sort();
    let vals: Vec<f64> = scores.iter().map(|s| s.as_f64()).collect();
    assert_eq!(vals, vec![-1.0, 0.0, 0.5, 1.0]);
}
