#![allow(clippy::unwrap_used)]
//! Integration tests for `TidalDb::create_backup`.

use std::collections::HashMap;
use std::sync::atomic::Ordering;
use std::time::Duration;

use tidaldb::schema::validation::DecaySpec;
use tidaldb::schema::validation::builders::SchemaBuilder;
use tidaldb::schema::{EntityId, EntityKind, Timestamp, Window};
use tidaldb::{TidalDb, TidalError};

/// Build a schema with a single "view" signal type.
fn test_schema() -> tidaldb::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])
        .velocity(false)
        .add();
    builder.build().unwrap()
}

/// Build a persistent test database at `path` with the standard schema.
fn build_persistent_db(path: &std::path::Path) -> TidalDb {
    TidalDb::builder()
        .with_data_dir(path)
        .with_schema(test_schema())
        .open()
        .unwrap()
}

#[test]
fn backup_creates_copy_of_data() {
    let src_dir = tempfile::tempdir().unwrap();
    let bak_dir = tempfile::tempdir().unwrap();
    let backup_path = bak_dir.path().join("backup");

    let db = build_persistent_db(src_dir.path());

    // Write some items using write_item_with_metadata so they appear in
    // the universe bitmap (which item_count() reads).
    for i in 1..=10 {
        let mut meta = HashMap::new();
        meta.insert("title".to_string(), format!("Item {i}"));
        db.write_item_with_metadata(EntityId::new(i), &meta)
            .unwrap();
    }

    // Write some signals.
    for i in 1..=10 {
        db.signal("view", EntityId::new(i), 1.0, Timestamp::now())
            .unwrap();
    }

    let info = db.create_backup(&backup_path).unwrap();
    assert!(info.size_bytes > 0, "backup should contain data");
    assert_eq!(info.path, backup_path);
    assert_eq!(info.item_count, 10);
    assert!(info.timestamp_ns > 0);

    // Close the original database (releases the lock).
    db.close().unwrap();

    // Open the backup and verify items survived in storage.
    // NOTE: item_count() reads the in-memory universe bitmap which is only
    // populated during write_item_with_metadata(); on reopen it starts empty
    // (bitmap indexes are not persisted). Instead, verify via get_item_metadata.
    let db2 = TidalDb::builder()
        .with_data_dir(&backup_path)
        .with_schema(test_schema())
        .open()
        .unwrap();
    for i in 1..=10 {
        let meta = db2.get_item_metadata(EntityId::new(i)).unwrap();
        assert!(meta.is_some(), "item {i} should exist in the backup");
        let meta = meta.unwrap();
        assert_eq!(
            meta.get("title").map(String::as_str),
            Some(format!("Item {i}").as_str()),
            "item {i} title should match"
        );
    }
    db2.close().unwrap();
}

#[test]
fn backup_fails_for_ephemeral_db() {
    let db = TidalDb::builder()
        .ephemeral()
        .with_schema(test_schema())
        .open()
        .unwrap();
    let bak_dir = tempfile::tempdir().unwrap();
    let result = db.create_backup(bak_dir.path());
    assert!(result.is_err());
    let err_msg = result.unwrap_err().to_string();
    assert!(
        err_msg.contains("persistent"),
        "expected persistent error, got: {err_msg}"
    );
}

#[test]
fn backup_preserves_signal_scores() {
    let src_dir = tempfile::tempdir().unwrap();
    let bak_dir = tempfile::tempdir().unwrap();
    let backup_path = bak_dir.path().join("backup");

    let db = build_persistent_db(src_dir.path());

    let entity = EntityId::new(42);
    let mut meta = HashMap::new();
    meta.insert("title".to_string(), "Test Item".to_string());
    db.write_item_with_metadata(entity, &meta).unwrap();

    // Record several signals to build up a score.
    for _ in 0..5 {
        db.signal("view", entity, 1.0, Timestamp::now()).unwrap();
    }

    // Read the score before backup.
    let score_before = db.read_decay_score(entity, "view", 0).unwrap();
    assert!(
        score_before.is_some(),
        "should have a score after 5 signals"
    );

    let _info = db.create_backup(&backup_path).unwrap();
    db.close().unwrap();

    // Open backup and verify scores survived.
    let db2 = TidalDb::builder()
        .with_data_dir(&backup_path)
        .with_schema(test_schema())
        .open()
        .unwrap();

    let score_after = db2.read_decay_score(entity, "view", 0).unwrap();
    assert!(
        score_after.is_some(),
        "backup should preserve signal scores"
    );

    // The scores should be very close (tiny decay between backup and reopen).
    // With a 7-day half-life, a few seconds of elapsed time produces
    // negligible decay, so we allow up to 0.5 drift.
    let before = score_before.unwrap();
    let after = score_after.unwrap();
    let diff = (before - after).abs();
    assert!(
        diff < 0.5,
        "signal score drift should be minimal: before={before}, after={after}, diff={diff}"
    );

    db2.close().unwrap();
}

#[test]
fn backup_returns_backpressure_during_backup() {
    // Verify the atomic flag mechanism works by manipulating it directly.
    // A true integration test with concurrent backup + signal is racy and
    // non-deterministic, so we test the invariant: when backup_in_progress
    // is true, signal() returns Backpressure.
    let src_dir = tempfile::tempdir().unwrap();
    let db = build_persistent_db(src_dir.path());

    // Manually set the backup flag (simulating an in-progress backup).
    db.backup_in_progress_flag().store(true, Ordering::Release);

    let result = db.signal("view", EntityId::new(1), 1.0, Timestamp::now());
    assert!(result.is_err());
    match result.unwrap_err() {
        TidalError::Backpressure { retry_after_ms } => {
            assert!(retry_after_ms > 0, "retry_after_ms should be positive");
        }
        other => panic!("expected Backpressure, got: {other}"),
    }

    // Clear the flag -- writes should succeed again.
    db.backup_in_progress_flag().store(false, Ordering::Release);

    let result = db.signal("view", EntityId::new(1), 1.0, Timestamp::now());
    assert!(
        result.is_ok(),
        "signal should succeed after backup completes"
    );
}

#[test]
fn backup_skips_lock_file() {
    let src_dir = tempfile::tempdir().unwrap();
    let bak_dir = tempfile::tempdir().unwrap();
    let backup_path = bak_dir.path().join("backup");

    let db = build_persistent_db(src_dir.path());

    // The lock file should exist in src_dir.
    assert!(
        src_dir.path().join("tidaldb.lock").exists(),
        "lock file should exist for persistent db"
    );

    let _info = db.create_backup(&backup_path).unwrap();

    // The lock file should NOT be in the backup.
    assert!(
        !backup_path.join("tidaldb.lock").exists(),
        "backup should not include tidaldb.lock"
    );

    db.close().unwrap();
}

#[test]
fn backup_concurrent_backup_rejected() {
    let src_dir = tempfile::tempdir().unwrap();
    let db = build_persistent_db(src_dir.path());

    // Set the flag to simulate an in-progress backup.
    db.backup_in_progress_flag().store(true, Ordering::Release);

    let bak_dir = tempfile::tempdir().unwrap();
    let result = db.create_backup(bak_dir.path());
    assert!(result.is_err());
    let err_msg = result.unwrap_err().to_string();
    assert!(
        err_msg.contains("already in progress"),
        "expected concurrent backup error, got: {err_msg}"
    );

    // Clean up the flag.
    db.backup_in_progress_flag().store(false, Ordering::Release);
}
