//! Integration tests for M7P4 Operational Visibility — session metrics + cross-session aggregation.
//!
//! - Task 04: Session + cohort + degradation metrics
//! - Task 09: Cross-session aggregation
#![allow(clippy::too_many_lines, clippy::unwrap_used)]

use std::{collections::HashMap, time::Duration};

use tidaldb::{
    TidalDb,
    schema::{DecaySpec, EntityId, EntityKind, Timestamp, Window},
};

// ── Prometheus output helpers ────────────────────────────────────────────────

/// Extract the numeric value for a metric from Prometheus text output.
///
/// Looks for a line that starts with `name` (not a comment), then parses
/// the trailing float. Returns `None` if the metric is absent.
#[cfg(feature = "metrics")]
fn prometheus_value(prom: &str, name: &str) -> Option<f64> {
    prom.lines()
        .filter(|l| !l.starts_with('#'))
        .find(|l| l.starts_with(name) && l[name.len()..].starts_with(' '))
        .and_then(|l| l.split_whitespace().last())
        .and_then(|v| v.parse().ok())
}

// ── Shared test schema ───────────────────────────────────────────────────────

fn build_test_schema() -> tidaldb::schema::Schema {
    use tidaldb::{AgentPolicy, schema::SchemaBuilder};

    let mut builder = SchemaBuilder::new();
    let _ = builder
        .signal(
            "view",
            EntityKind::Item,
            DecaySpec::Exponential {
                half_life: Duration::from_secs(7 * 24 * 3600),
            },
        )
        .windows(&[Window::OneHour])
        .add();
    let _ = builder
        .signal(
            "like",
            EntityKind::Item,
            DecaySpec::Exponential {
                half_life: Duration::from_secs(30 * 24 * 3600),
            },
        )
        .windows(&[Window::OneHour])
        .add();
    builder.session_policy(
        "default",
        AgentPolicy {
            allowed_signals: vec!["view".to_string(), "like".to_string()],
            denied_signals: vec![],
            max_session_duration: Duration::from_secs(3600),
            max_signals_per_session: 1000,
        },
    );
    builder.build().unwrap()
}

// ── Task 04: Session + Cohort + Degradation Metrics ─────────────────────────

#[cfg(feature = "metrics")]
#[test]
fn session_start_increments_active_sessions() {
    let schema = build_test_schema();
    let db = TidalDb::builder()
        .ephemeral()
        .with_schema(schema)
        .open()
        .unwrap();

    let before = prometheus_value(&db.metrics().render_prometheus(), "tidaldb_active_sessions")
        .unwrap_or(0.0);
    let meta = HashMap::new();
    let _handle = db.start_session(1u64, "agent1", "default", meta).unwrap();
    let after =
        prometheus_value(&db.metrics().render_prometheus(), "tidaldb_active_sessions").unwrap();

    assert!(
        ((after - before) - 1.0).abs() < 0.5,
        "active_sessions must increment when a session starts"
    );
}

#[cfg(feature = "metrics")]
#[test]
fn session_close_updates_active_and_closed_counters() {
    let schema = build_test_schema();
    let db = TidalDb::builder()
        .ephemeral()
        .with_schema(schema)
        .open()
        .unwrap();

    let before_active =
        prometheus_value(&db.metrics().render_prometheus(), "tidaldb_active_sessions")
            .unwrap_or(0.0);
    let before_closed = prometheus_value(
        &db.metrics().render_prometheus(),
        "tidaldb_closed_sessions_total",
    )
    .unwrap_or(0.0);

    let meta = HashMap::new();
    let handle = db
        .start_session(42u64, "agent-close-test", "default", meta)
        .unwrap();
    db.close_session(handle).unwrap();

    let after_active =
        prometheus_value(&db.metrics().render_prometheus(), "tidaldb_active_sessions").unwrap();
    let after_closed = prometheus_value(
        &db.metrics().render_prometheus(),
        "tidaldb_closed_sessions_total",
    )
    .unwrap();

    assert!(
        (after_active - before_active).abs() < 0.5,
        "active_sessions must return to its previous value after close_session"
    );
    assert!(
        ((after_closed - before_closed) - 1.0).abs() < 0.5,
        "closed_sessions_total must increment on close_session"
    );
}

#[cfg(feature = "metrics")]
#[test]
fn degradation_level_appears_in_prometheus_output() {
    let schema = build_test_schema();
    let db = TidalDb::builder()
        .ephemeral()
        .with_schema(schema)
        .open()
        .unwrap();

    let prom = db.metrics().render_prometheus();

    // Default: Full fidelity (level 0).
    let level = prometheus_value(&prom, "tidaldb_degradation_level").unwrap();
    assert!(
        level.abs() < 0.5,
        "initial degradation level must be 0 (Full fidelity)"
    );

    assert!(
        prom.contains("tidaldb_degradation_level"),
        "Prometheus output must include degradation_level gauge"
    );
    assert!(
        prom.contains("tidaldb_rate_limited_total"),
        "Prometheus output must include rate_limited_total counter"
    );
}

// ── Task 09: Cross-Session Aggregation ──────────────────────────────────────

#[test]
fn user_session_summary_aggregates_correctly() {
    let schema = build_test_schema();
    let db = TidalDb::builder()
        .ephemeral()
        .with_schema(schema)
        .open()
        .unwrap();
    let user_id = 42u64;
    let meta = HashMap::new();

    // Run 3 sessions, each with 2 views + 1 like = 3 signals per session.
    for i in 0u64..3 {
        let handle = db
            .start_session(user_id, "test", "default", meta.clone())
            .unwrap();
        db.session_signal(
            &handle,
            "view",
            EntityId::new(i * 10 + 1),
            1.0,
            Timestamp::now(),
            None,
        )
        .unwrap();
        db.session_signal(
            &handle,
            "view",
            EntityId::new(i * 10 + 2),
            1.0,
            Timestamp::now(),
            None,
        )
        .unwrap();
        db.session_signal(
            &handle,
            "like",
            EntityId::new(i * 10 + 3),
            1.0,
            Timestamp::now(),
            None,
        )
        .unwrap();
        db.close_session(handle).unwrap();
    }

    let summary = db.user_session_summary(user_id, 0).unwrap();
    assert_eq!(summary.sessions_count, 3);
    assert_eq!(summary.total_signals, 9);
    assert_eq!(summary.total_rejections, 0);
    assert_eq!(summary.user_id, user_id);
    assert_eq!(summary.since_ns, 0);
    assert!(summary.earliest_session_ns.is_some());
    assert!(summary.latest_session_ns.is_some());

    // Top signal types: "view" first (6 total), "like" second (3 total).
    assert_eq!(summary.top_signal_types.len(), 2);
    assert_eq!(summary.top_signal_types[0].0, "view");
    assert_eq!(summary.top_signal_types[0].1, 6);
    assert_eq!(summary.top_signal_types[1].0, "like");
    assert_eq!(summary.top_signal_types[1].1, 3);
}

#[test]
fn user_session_summary_no_sessions_returns_not_found() {
    let schema = build_test_schema();
    let db = TidalDb::builder()
        .ephemeral()
        .with_schema(schema)
        .open()
        .unwrap();

    let result = db.user_session_summary(999, 0);
    assert!(result.is_err());
    assert!(matches!(result, Err(tidaldb::TidalError::NotFound { .. })));
}

#[test]
fn user_session_summary_different_user_excluded() {
    let schema = build_test_schema();
    let db = TidalDb::builder()
        .ephemeral()
        .with_schema(schema)
        .open()
        .unwrap();
    let meta = HashMap::new();

    // User 1: one view signal.
    let handle = db
        .start_session(1u64, "test", "default", meta.clone())
        .unwrap();
    db.session_signal(
        &handle,
        "view",
        EntityId::new(1),
        1.0,
        Timestamp::now(),
        None,
    )
    .unwrap();
    db.close_session(handle).unwrap();

    // User 2: one like signal.
    let handle = db.start_session(2u64, "test", "default", meta).unwrap();
    db.session_signal(
        &handle,
        "like",
        EntityId::new(2),
        1.0,
        Timestamp::now(),
        None,
    )
    .unwrap();
    db.close_session(handle).unwrap();

    // Query user 1 only — user 2's session must be excluded.
    let summary = db.user_session_summary(1, 0).unwrap();
    assert_eq!(summary.sessions_count, 1);
    assert_eq!(summary.total_signals, 1);
    assert_eq!(summary.top_signal_types.len(), 1);
    assert_eq!(summary.top_signal_types[0].0, "view");
    assert_eq!(summary.top_signal_types[0].1, 1);
}

#[test]
fn user_session_summary_since_ns_filter() {
    let schema = build_test_schema();
    let db = TidalDb::builder()
        .ephemeral()
        .with_schema(schema)
        .open()
        .unwrap();
    let user_id = 100u64;
    let meta = HashMap::new();

    // Session 1.
    let handle = db
        .start_session(user_id, "test", "default", meta.clone())
        .unwrap();
    db.session_signal(
        &handle,
        "view",
        EntityId::new(1),
        1.0,
        Timestamp::now(),
        None,
    )
    .unwrap();
    db.close_session(handle).unwrap();

    // Record time between sessions.
    let midpoint = Timestamp::now().as_nanos();

    // Session 2 (after midpoint).
    let handle = db.start_session(user_id, "test", "default", meta).unwrap();
    db.session_signal(
        &handle,
        "like",
        EntityId::new(2),
        1.0,
        Timestamp::now(),
        None,
    )
    .unwrap();
    db.close_session(handle).unwrap();

    // since_ns = midpoint must exclude session 1 and include only session 2.
    let summary = db.user_session_summary(user_id, midpoint).unwrap();
    assert_eq!(summary.sessions_count, 1);
    assert_eq!(summary.total_signals, 1);
    assert_eq!(summary.top_signal_types[0].0, "like");
}

#[test]
fn session_snapshot_has_timing_fields() {
    let schema = build_test_schema();
    let db = TidalDb::builder()
        .ephemeral()
        .with_schema(schema)
        .open()
        .unwrap();
    let user_id = 50u64;
    let meta = HashMap::new();

    let before = Timestamp::now().as_nanos();

    let handle = db.start_session(user_id, "test", "default", meta).unwrap();
    db.session_signal(
        &handle,
        "view",
        EntityId::new(1),
        1.0,
        Timestamp::now(),
        None,
    )
    .unwrap();

    // Active snapshot: started_at_ns populated, closed_at_ns = 0.
    let active_snap = db.session_snapshot(handle.id).unwrap();
    assert!(
        active_snap.started_at_ns >= before,
        "started_at_ns should be after test start"
    );
    assert_eq!(
        active_snap.closed_at_ns, 0,
        "active session should have closed_at_ns = 0"
    );

    db.close_session(handle).unwrap();

    let after = Timestamp::now().as_nanos();

    // Summary timing should bracket the session.
    let summary = db.user_session_summary(user_id, 0).unwrap();
    assert!(summary.earliest_session_ns.unwrap() >= before);
    assert!(summary.latest_session_ns.unwrap() <= after);
}

#[test]
fn signal_snap_entry_has_count() {
    let schema = build_test_schema();
    let db = TidalDb::builder()
        .ephemeral()
        .with_schema(schema)
        .open()
        .unwrap();
    let user_id = 60u64;
    let meta = HashMap::new();

    let handle = db.start_session(user_id, "test", "default", meta).unwrap();
    // Write 3 view signals and 1 like signal.
    for entity in [1u64, 2, 3] {
        db.session_signal(
            &handle,
            "view",
            EntityId::new(entity),
            1.0,
            Timestamp::now(),
            None,
        )
        .unwrap();
    }
    db.session_signal(
        &handle,
        "like",
        EntityId::new(4),
        1.0,
        Timestamp::now(),
        None,
    )
    .unwrap();

    let snap = db.session_snapshot(handle.id).unwrap();
    let view_entry = snap.signals.get("view").unwrap();
    assert_eq!(view_entry.count, 3);
    let like_entry = snap.signals.get("like").unwrap();
    assert_eq!(like_entry.count, 1);

    db.close_session(handle).unwrap();
}
