use std::fs::{self, File};
use std::io::Write;
use std::path::Path;
use std::time::SystemTime;
use tempfile::TempDir;
use stagecrew::audit::{AuditAction, AuditActorSource, AuditEvent, AuditService};
use stagecrew::config::Config;
use stagecrew::db::Database;
use stagecrew::removal::remove_approved;
use stagecrew::scanner::{Scanner, scan_and_persist, transition_expired_paths};
#[allow(clippy::too_many_lines)]
#[tokio::test]
async fn test_full_workflow() {
let temp_root = TempDir::with_prefix("stagecrew-integration-").expect("failed to create integration test temp directory - check disk space and system temp directory permissions");
let db_path = temp_root.path().join("test.db");
let tracked_dir = temp_root.path().join("staging");
fs::create_dir_all(&tracked_dir).expect("failed to create staging directory for integration test - check disk space and write permissions");
let old_file = tracked_dir.join("old_data.txt");
let recent_file = tracked_dir.join("recent_data.txt");
let middle_file = tracked_dir.join("middle_data.txt");
create_file_with_age(&old_file, 95, 1024);
create_file_with_age(&recent_file, 10, 512);
create_file_with_age(&middle_file, 80, 4096);
let db = Database::open(&db_path).expect("database should initialize");
let mut config = Config::default();
config.tracked_paths = vec![tracked_dir.clone()];
config.expiration_days = 90;
config.warning_days = 14;
config.auto_remove = false;
let scanner = Scanner::new();
let app_config = stagecrew::config::AppConfig::from_global(config.clone());
let scan_summary = scan_and_persist(&db, &scanner, &app_config)
.await
.expect("scan should succeed");
assert_eq!(
scan_summary.total_directories, 1,
"should scan 1 directory (the tracked root)"
);
assert_eq!(scan_summary.total_files, 3, "should scan 3 files");
let expected_total_bytes = 1024 + 512 + 4096;
assert_eq!(
scan_summary.total_size_bytes, expected_total_bytes,
"should count all bytes"
);
let roots = db.list_roots().expect("should list roots");
assert_eq!(roots.len(), 1, "should have 1 root in database");
assert_eq!(roots[0].path, tracked_dir, "root path should match");
let all_entries = db
.list_entries_by_parent(roots[0].id, &tracked_dir)
.expect("should list entries");
assert_eq!(
all_entries.len(),
3,
"should have 3 file entries in database"
);
for entry in &all_entries {
assert_eq!(
entry.status, "tracked",
"entries should start with tracked status"
);
}
let old_file_entry = all_entries
.iter()
.find(|e| e.path == old_file)
.expect("old_file should be in database");
assert_eq!(
old_file_entry.size_bytes, 1024,
"old_file should have correct size"
);
assert!(old_file_entry.mtime.is_some(), "old_file should have mtime");
let now = jiff::Timestamp::now();
let ninetyfive_days_ago = now
.checked_sub(jiff::SignedDuration::from_secs(95 * 86400))
.expect("timestamp arithmetic");
let old_file_str = old_file.to_string_lossy();
db.conn()
.execute(
"UPDATE entries SET countdown_start = ?1 WHERE path = ?2",
(ninetyfive_days_ago.as_second(), &*old_file_str),
)
.expect("failed to backdate countdown_start for old_file");
let transition_summary =
transition_expired_paths(&db, &app_config).expect("transition should succeed");
assert_eq!(
transition_summary.expired_to_pending, 1,
"should transition 1 entry to pending"
);
assert_eq!(
transition_summary.expired_to_approved, 0,
"should not auto-approve (auto_remove=false)"
);
let pending_entries = db
.list_entries(Some("pending"))
.expect("should list pending entries");
assert_eq!(
pending_entries.len(),
1,
"should have 1 pending entry after transition"
);
assert_eq!(
pending_entries[0].path, old_file,
"old_file should be pending"
);
let audit = AuditService::new(&db);
let user = AuditService::current_user();
db.update_entry_status(old_file_entry.id, "approved")
.expect("should update status to approved");
audit
.record_event(&AuditEvent {
user: &user,
actor_source: AuditActorSource::Tui,
action: AuditAction::Approve,
target_path: Some(old_file.as_path()),
details: Some("Manual approval for removal"),
entry_id: Some(old_file_entry.id),
root_id: None,
status_before: Some("tracked"),
status_after: Some("approved"),
outcome: Some("approved"),
})
.expect("should record approval in audit log");
let approved_entries = db
.list_entries(Some("approved"))
.expect("should list approved entries");
assert_eq!(approved_entries.len(), 1, "should have 1 approved entry");
assert_eq!(
approved_entries[0].path, old_file,
"old_file should be approved"
);
let removal_summary = remove_approved(&db).expect("removal should succeed");
assert_eq!(removal_summary.removed_count(), 1, "should remove 1 entry");
assert_eq!(
removal_summary.blocked_count(),
0,
"should have 0 blocked removals"
);
assert_eq!(
removal_summary.total_bytes_freed(),
1024,
"should free correct number of bytes"
);
assert!(
!old_file.exists(),
"old_file should no longer exist on filesystem"
);
let removed_entries = db
.list_entries(Some("removed"))
.expect("should list removed entries");
assert_eq!(
removed_entries.len(),
1,
"should have 1 removed entry in database"
);
assert_eq!(
removed_entries[0].path, old_file,
"old_file should have removed status"
);
let audit_entries = audit
.list_recent(10)
.expect("should list recent audit entries");
assert!(
audit_entries.len() >= 3,
"should have at least 3 audit entries (scan, approve, remove), got {}",
audit_entries.len()
);
let actions: Vec<String> = audit_entries.iter().map(|e| e.action.clone()).collect();
assert!(
actions.contains(&"scan".to_string()),
"should have scan action"
);
assert!(
actions.contains(&"approve".to_string()),
"should have approve action"
);
assert!(
actions.contains(&"remove".to_string()),
"should have remove action"
);
let approve_entry = audit_entries
.iter()
.find(|e| e.action == "approve")
.expect("should find approve entry");
assert_eq!(
approve_entry.target_path,
Some(old_file.to_string_lossy().into_owned()),
"approve entry should reference old_file"
);
assert_eq!(
approve_entry.entry_id,
Some(old_file_entry.id),
"approve entry should have entry_id"
);
let remove_entry = audit_entries
.iter()
.find(|e| e.action == "remove")
.expect("should find remove entry");
assert!(
remove_entry
.details
.as_ref()
.expect("remove audit entry should have details field populated with bytes freed - check removal service records details correctly")
.contains("1024"),
"remove entry should mention bytes freed"
);
assert!(
recent_file.exists(),
"recent_file should still exist (not expired)"
);
assert!(
middle_file.exists(),
"middle_file should still exist (within warning period)"
);
let tracked_entries: Vec<_> = db
.list_entries(Some("tracked"))
.expect("should list tracked entries")
.into_iter()
.filter(|e| !e.is_dir)
.collect();
assert_eq!(
tracked_entries.len(),
2,
"should still have 2 tracked file entries (recent and middle files)"
);
let stats = db.get_stats().expect("should get stats");
assert_eq!(stats.total_files, 3, "stats should show 3 total files");
assert!(
stats.last_scan_completed.is_some(),
"stats should have last_scan_completed timestamp"
);
}
fn create_file_with_age(path: &Path, days_ago: u64, size_bytes: usize) {
let mut file = File::create(path).expect("should create file");
let data = vec![b'X'; size_bytes];
file.write_all(&data).expect("should write data");
file.flush().expect("should flush file");
let now = SystemTime::now();
let age_seconds = days_ago * 86400;
let mtime = now
.checked_sub(std::time::Duration::from_secs(age_seconds))
.expect("should calculate past time");
filetime::set_file_mtime(path, filetime::FileTime::from_system_time(mtime))
.expect("should set mtime");
}