//! Anti-forensics and threat detection from USN Journal records.
//!
//! Provides heuristic detectors for:
//! - Secure deletion tool artifacts (`SDelete`, `CCleaner`, cipher /w)
//! - USN journal clearing / tampering
//! - Ransomware-like mass rename/encrypt patterns
//! - Timestamp manipulation (timestomping)

use std::collections::HashMap;

use chrono::{DateTime, Duration, Utc};

use ntfs_core::usn::{UsnReason, UsnRecord};

// ═══════════════════════════════════════════════════════════════════════════════
// Secure Deletion Detection
// ═══════════════════════════════════════════════════════════════════════════════

/// Indicator of secure deletion tool usage.
#[derive(Debug, Clone)]
pub struct SecureDeletionIndicator {
    /// The type of secure deletion pattern detected.
    pub pattern: SecureDeletionPattern,
    /// Filenames involved in the pattern.
    pub filenames: Vec<String>,
    /// Time window during which the pattern was observed.
    pub time_start: DateTime<Utc>,
    /// End of the time window during which the pattern was observed.
    pub time_end: DateTime<Utc>,
    /// Confidence score (0.0 - 1.0).
    pub confidence: f64,
}

/// Known secure deletion tool patterns.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SecureDeletionPattern {
    /// `SDelete` creates files named with repeating chars (AAA, ZZZ, 000) then deletes them.
    SDelete,
    /// `CCleaner` and similar tools create/delete many .tmp files rapidly.
    BulkTempDeletion,
    /// `cipher /w` creates large files to overwrite free space.
    CipherWipe,
}

/// Detect patterns indicative of secure deletion tools.
///
/// Looks for:
/// - Rapid create-delete cycles for files named with `SDelete` patterns (AAA, ZZZ, 000)
/// - Bulk .tmp file deletion in Temp folders
#[must_use]
pub fn detect_secure_deletion(records: &[UsnRecord]) -> Vec<SecureDeletionIndicator> {
    let mut indicators = Vec::new();

    // ── SDelete detection ────────────────────────────────────────────────
    // SDelete renames files to sequences of repeating characters before deletion.
    // Look for create+delete of files matching patterns like AAAAAA, ZZZZZZ, 000000.

    let sdelete_patterns = detect_sdelete_patterns(records);
    indicators.extend(sdelete_patterns);

    // ── Bulk temp file deletion ──────────────────────────────────────────
    let temp_indicators = detect_bulk_temp_deletion(records);
    indicators.extend(temp_indicators);

    indicators
}

/// Detect `SDelete`-style repeating character filename patterns.
fn detect_sdelete_patterns(records: &[UsnRecord]) -> Vec<SecureDeletionIndicator> {
    let mut indicators = Vec::new();

    // Collect files matching SDelete naming patterns with create or delete events
    let mut sdelete_events: Vec<&UsnRecord> = Vec::new();

    for record in records {
        if is_sdelete_filename(&record.filename)
            && (record.reason.contains(UsnReason::FILE_CREATE)
                || record.reason.contains(UsnReason::FILE_DELETE))
        {
            sdelete_events.push(record);
        }
    }

    if sdelete_events.len() < 3 {
        return indicators;
    }

    // Group by time windows (within 60 seconds)
    let mut groups: Vec<Vec<&UsnRecord>> = Vec::new();
    let mut current_group: Vec<&UsnRecord> = vec![sdelete_events[0]];

    for event in &sdelete_events[1..] {
        let within_window = current_group
            .last()
            .is_some_and(|last| event.timestamp - last.timestamp <= Duration::seconds(60));
        if within_window {
            current_group.push(event);
        } else {
            if current_group.len() >= 3 {
                groups.push(std::mem::take(&mut current_group));
            } else {
                current_group.clear();
            }
            current_group.push(event);
        }
    }
    if current_group.len() >= 3 {
        groups.push(current_group);
    }

    for group in groups {
        let filenames: Vec<String> = group.iter().map(|r| r.filename.clone()).collect();
        let Some(time_start) = group.first().map(|r| r.timestamp) else {
            continue; // cov:unreachable: group is non-empty (only groups with len>=3 are built), so first() is always Some
        };
        let Some(time_end) = group.last().map(|r| r.timestamp) else {
            continue; // cov:unreachable: group is non-empty (only groups with len>=3 are built), so last() is always Some
        };

        // Higher confidence if we see both creates and deletes
        let has_creates = group
            .iter()
            .any(|r| r.reason.contains(UsnReason::FILE_CREATE));
        let has_deletes = group
            .iter()
            .any(|r| r.reason.contains(UsnReason::FILE_DELETE));
        let confidence = if has_creates && has_deletes { 0.9 } else { 0.6 };

        indicators.push(SecureDeletionIndicator {
            pattern: SecureDeletionPattern::SDelete,
            filenames,
            time_start,
            time_end,
            confidence,
        });
    }

    indicators
}

/// Check if a filename matches `SDelete`'s repeating character pattern.
fn is_sdelete_filename(name: &str) -> bool {
    // Strip extension if present
    let base = name.split('.').next().unwrap_or(name);
    if base.len() < 3 {
        return false;
    }
    let Some(first) = base.chars().next() else {
        return false; // cov:unreachable: base has >=3 bytes (checked above), so chars().next() is always Some
    };
    // SDelete uses repeating single characters: AAA, ZZZ, 000
    base.chars().all(|c| c == first) && (first.is_ascii_uppercase() || first.is_ascii_digit())
}

/// Detect bulk .tmp file deletion indicative of cleaning tools.
fn detect_bulk_temp_deletion(records: &[UsnRecord]) -> Vec<SecureDeletionIndicator> {
    let mut indicators = Vec::new();

    // Find delete events for .tmp files
    let tmp_deletes: Vec<&UsnRecord> = records
        .iter()
        .filter(|r| {
            r.reason.contains(UsnReason::FILE_DELETE) && r.filename.to_lowercase().ends_with(".tmp")
        })
        .collect();

    if tmp_deletes.len() < 10 {
        return indicators;
    }

    // Group by 30-second windows
    let mut groups: Vec<Vec<&UsnRecord>> = Vec::new();
    let mut current_group: Vec<&UsnRecord> = vec![tmp_deletes[0]];

    for event in &tmp_deletes[1..] {
        let within_window = current_group
            .last()
            .is_some_and(|last| event.timestamp - last.timestamp <= Duration::seconds(30));
        if within_window {
            current_group.push(event);
        } else {
            if current_group.len() >= 10 {
                groups.push(std::mem::take(&mut current_group));
            } else {
                current_group.clear();
            }
            current_group.push(event);
        }
    }
    if current_group.len() >= 10 {
        groups.push(current_group);
    }

    for group in groups {
        let Some(time_start) = group.first().map(|r| r.timestamp) else {
            continue; // cov:unreachable: group is non-empty (only groups with len>=10 are built), so first() is always Some
        };
        let Some(time_end) = group.last().map(|r| r.timestamp) else {
            continue; // cov:unreachable: group is non-empty (only groups with len>=10 are built), so last() is always Some
        };
        indicators.push(SecureDeletionIndicator {
            pattern: SecureDeletionPattern::BulkTempDeletion,
            filenames: group.iter().map(|r| r.filename.clone()).collect(),
            time_start,
            time_end,
            confidence: 0.7,
        });
    }

    indicators
}

// ═══════════════════════════════════════════════════════════════════════════════
// Journal Clearing Detection
// ═══════════════════════════════════════════════════════════════════════════════

/// Result of journal clearing analysis.
#[derive(Debug, Clone)]
pub struct JournalClearingResult {
    /// Whether journal clearing was detected.
    pub clearing_detected: bool,
    /// The first USN value seen (high values suggest prior clearing).
    pub first_usn: Option<i64>,
    /// Timestamp gaps detected (sudden jumps in time).
    pub timestamp_gaps: Vec<TimestampGap>,
    /// Overall confidence (0.0 - 1.0).
    pub confidence: f64,
}

/// A detected gap in the journal timeline.
#[derive(Debug, Clone)]
pub struct TimestampGap {
    /// Timestamp before the gap.
    pub before: DateTime<Utc>,
    /// Timestamp after the gap.
    pub after: DateTime<Utc>,
    /// Duration of the gap.
    pub gap_duration: Duration,
    /// USN value before the gap.
    pub usn_before: i64,
    /// USN value after the gap.
    pub usn_after: i64,
}

/// Detect if the USN journal was cleared or tampered with.
///
/// Indicators:
/// - First record's USN is very high (older records were removed)
/// - Sudden large timestamp jumps between consecutive records
#[must_use]
pub fn detect_journal_clearing(records: &[UsnRecord]) -> JournalClearingResult {
    let Some(first) = records.first() else {
        return JournalClearingResult {
            clearing_detected: false,
            first_usn: None,
            timestamp_gaps: Vec::new(),
            confidence: 0.0,
        };
    };

    let first_usn = first.usn;
    let mut confidence = 0.0;

    // A high first USN suggests the journal has been in use but older entries were cleared.
    // Typical threshold: if first USN > 1GB of journal data, it's suspicious.
    const USN_CLEARING_THRESHOLD: i64 = 1_073_741_824; // 1 GiB
    let high_usn = first_usn > USN_CLEARING_THRESHOLD;
    if high_usn {
        confidence += 0.5;
    }

    // Detect timestamp gaps (jumps of > 24 hours between consecutive records)
    let mut timestamp_gaps = Vec::new();
    let gap_threshold = Duration::hours(24);

    for window in records.windows(2) {
        let gap = window[1].timestamp - window[0].timestamp;
        if gap > gap_threshold {
            timestamp_gaps.push(TimestampGap {
                before: window[0].timestamp,
                after: window[1].timestamp,
                gap_duration: gap,
                usn_before: window[0].usn,
                usn_after: window[1].usn,
            });
        }
    }

    if !timestamp_gaps.is_empty() {
        // More gaps = higher confidence
        let gap_factor = (timestamp_gaps.len() as f64 * 0.2).min(0.5);
        confidence += gap_factor;
    }

    let clearing_detected = confidence >= 0.4;

    JournalClearingResult {
        clearing_detected,
        first_usn: Some(first_usn),
        timestamp_gaps,
        confidence,
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// Ransomware Detection
// ═══════════════════════════════════════════════════════════════════════════════

/// Indicator of ransomware-like activity.
#[derive(Debug, Clone)]
pub struct RansomwareIndicator {
    /// The suspicious extension being added to files.
    pub extension: String,
    /// Number of files affected.
    pub affected_count: usize,
    /// Sample filenames that were renamed.
    pub sample_filenames: Vec<String>,
    /// Time window of the activity.
    pub time_start: DateTime<Utc>,
    /// End of the time window of the activity.
    pub time_end: DateTime<Utc>,
    /// Confidence score (0.0 - 1.0).
    pub confidence: f64,
}

/// Known ransomware extensions to check for.
const RANSOMWARE_EXTENSIONS: &[&str] = &[
    ".encrypted",
    ".locked",
    ".crypto",
    ".crypt",
    ".enc",
    ".locky",
    ".cerber",
    ".zepto",
    ".odin",
    ".thor",
    ".aesir",
    ".zzzzz",
    ".micro",
    ".crypted",
    ".crinf",
    ".r5a",
    ".xrtn",
    ".xtbl",
    ".crypz",
    ".cryp1",
    ".ransom",
    ".wallet",
    ".onion",
    ".wncry",
    ".wcry",
    ".wncryt",
];

/// Detect ransomware-like behavior patterns in USN records.
///
/// Looks for:
/// - Mass file renames with known ransomware extensions
/// - High rate of `DATA_OVERWRITE` followed by `RENAME_NEW_NAME`
/// - Many files getting the same new extension in a short time window
#[must_use]
pub fn detect_ransomware_patterns(records: &[UsnRecord]) -> Vec<RansomwareIndicator> {
    let mut indicators = Vec::new();

    // ── Known extension detection ────────────────────────────────────────
    indicators.extend(detect_known_ransomware_extensions(records));

    // ── Mass rename with same new extension ──────────────────────────────
    indicators.extend(detect_mass_rename_patterns(records));

    indicators
}

/// Detect files being renamed to known ransomware extensions.
fn detect_known_ransomware_extensions(records: &[UsnRecord]) -> Vec<RansomwareIndicator> {
    let mut indicators = Vec::new();

    // Group renames by extension
    let mut extension_groups: HashMap<String, Vec<&UsnRecord>> = HashMap::new();

    for record in records {
        if record.reason.contains(UsnReason::RENAME_NEW_NAME) {
            let lower = record.filename.to_lowercase();
            for ext in RANSOMWARE_EXTENSIONS {
                if lower.ends_with(ext) {
                    extension_groups
                        .entry((*ext).to_string())
                        .or_default()
                        .push(record);
                    break;
                }
            }
        }
    }

    for (ext, group) in &extension_groups {
        if group.len() >= 3 {
            let Some(time_start) = group.iter().map(|r| r.timestamp).min() else {
                continue; // cov:unreachable: the enclosing len()>=3 check guarantees a non-empty iterator, so min() is always Some
            };
            let Some(time_end) = group.iter().map(|r| r.timestamp).max() else {
                continue; // cov:unreachable: the enclosing len()>=3 check guarantees a non-empty iterator, so max() is always Some
            };
            let sample: Vec<String> = group.iter().take(10).map(|r| r.filename.clone()).collect();

            let confidence = if group.len() >= 20 {
                0.95
            } else if group.len() >= 10 {
                0.85
            } else {
                0.6
            };

            indicators.push(RansomwareIndicator {
                extension: ext.clone(),
                affected_count: group.len(),
                sample_filenames: sample,
                time_start,
                time_end,
                confidence,
            });
        }
    }

    indicators
}

/// Detect mass rename patterns where many files get the same new extension.
fn detect_mass_rename_patterns(records: &[UsnRecord]) -> Vec<RansomwareIndicator> {
    let mut indicators = Vec::new();

    // Find DATA_OVERWRITE followed by RENAME_NEW_NAME patterns
    // Group renames by new extension in 5-minute windows
    let rename_records: Vec<&UsnRecord> = records
        .iter()
        .filter(|r| r.reason.contains(UsnReason::RENAME_NEW_NAME))
        .collect();

    if rename_records.len() < 20 {
        return indicators;
    }

    // Group by extension (excluding known ransomware - already handled above)
    let mut ext_groups: HashMap<String, Vec<&UsnRecord>> = HashMap::new();

    for record in &rename_records {
        if let Some(dot_pos) = record.filename.rfind('.') {
            let ext = record.filename[dot_pos..].to_lowercase();
            // Skip common extensions
            if !is_common_extension(&ext) {
                let lower = ext.clone();
                let is_known = RANSOMWARE_EXTENSIONS.iter().any(|&re| lower == re);
                if !is_known {
                    ext_groups.entry(ext).or_default().push(record);
                }
            }
        }
    }

    // Report extensions with abnormally high rename counts in tight time windows
    for (ext, group) in &ext_groups {
        if group.len() >= 20 {
            let Some(time_start) = group.iter().map(|r| r.timestamp).min() else {
                continue; // cov:unreachable: the enclosing len()>=20 check guarantees a non-empty iterator, so min() is always Some
            };
            let Some(time_end) = group.iter().map(|r| r.timestamp).max() else {
                continue; // cov:unreachable: the enclosing len()>=20 check guarantees a non-empty iterator, so max() is always Some
            };
            let duration = time_end - time_start;

            // 20+ renames to the same unusual extension within 10 minutes is suspicious
            if duration <= Duration::minutes(10) {
                let sample: Vec<String> =
                    group.iter().take(10).map(|r| r.filename.clone()).collect();

                indicators.push(RansomwareIndicator {
                    extension: ext.clone(),
                    affected_count: group.len(),
                    sample_filenames: sample,
                    time_start,
                    time_end,
                    confidence: 0.75,
                });
            }
        }
    }

    indicators
}

/// Check if an extension is common/benign.
fn is_common_extension(ext: &str) -> bool {
    matches!(
        ext,
        ".txt"
            | ".doc"
            | ".docx"
            | ".xls"
            | ".xlsx"
            | ".pdf"
            | ".jpg"
            | ".jpeg"
            | ".png"
            | ".gif"
            | ".mp3"
            | ".mp4"
            | ".avi"
            | ".zip"
            | ".rar"
            | ".exe"
            | ".dll"
            | ".sys"
            | ".log"
            | ".tmp"
            | ".bak"
            | ".html"
            | ".htm"
            | ".css"
            | ".js"
            | ".py"
            | ".rs"
            | ".c"
            | ".h"
            | ".cpp"
            | ".java"
            | ".xml"
            | ".json"
            | ".csv"
            | ".ppt"
            | ".pptx"
    )
}

// ═══════════════════════════════════════════════════════════════════════════════
// Timestomping Detection
// ═══════════════════════════════════════════════════════════════════════════════

/// Indicator of timestamp manipulation.
#[derive(Debug, Clone)]
pub struct TimestompIndicator {
    /// The filename whose timestamps may have been manipulated.
    pub filename: String,
    /// MFT entry number.
    pub mft_entry: u64,
    /// The timestamp of the `BASIC_INFO_CHANGE` event.
    pub change_timestamp: DateTime<Utc>,
    /// Whether data modification events were found nearby.
    pub has_nearby_data_change: bool,
    /// Confidence score (0.0 - 1.0).
    pub confidence: f64,
}

/// Detect timestamp manipulation from USN records.
///
/// Timestomping is often visible in USN journals because:
/// - Modifying `$STANDARD_INFORMATION` timestamps generates a `BASIC_INFO_CHANGE` event
/// - Legitimate timestamp changes usually accompany data modifications
/// - Isolated `BASIC_INFO_CHANGE` events without nearby data changes are suspicious
#[must_use]
pub fn detect_timestomping(records: &[UsnRecord]) -> Vec<TimestompIndicator> {
    let mut indicators = Vec::new();

    // Build a map of MFT entry -> records for correlation
    let mut entry_events: HashMap<u64, Vec<&UsnRecord>> = HashMap::new();
    for record in records {
        entry_events
            .entry(record.mft_entry)
            .or_default()
            .push(record);
    }

    // For each file, look for isolated BASIC_INFO_CHANGE events
    for (&mft_entry, events) in &entry_events {
        for (i, event) in events.iter().enumerate() {
            if !event.reason.contains(UsnReason::BASIC_INFO_CHANGE) {
                continue;
            }

            // Check if this BASIC_INFO_CHANGE has no accompanying data changes
            // within a window of nearby events (5 events before/after or 60 seconds)
            let has_nearby_data_change = events.iter().enumerate().any(|(j, other)| {
                if i == j {
                    return false;
                }
                let time_diff = if other.timestamp >= event.timestamp {
                    other.timestamp - event.timestamp
                } else {
                    event.timestamp - other.timestamp
                };
                if time_diff > Duration::seconds(60) {
                    return false;
                }
                other.reason.contains(UsnReason::DATA_OVERWRITE)
                    || other.reason.contains(UsnReason::DATA_EXTEND)
                    || other.reason.contains(UsnReason::DATA_TRUNCATION)
                    || other.reason.contains(UsnReason::FILE_CREATE)
            });

            // Isolated BASIC_INFO_CHANGE is suspicious
            if !has_nearby_data_change {
                // Check if reason is ONLY BASIC_INFO_CHANGE (possibly with CLOSE)
                let reason_without_close = event.reason & !UsnReason::CLOSE;
                let is_isolated = reason_without_close == UsnReason::BASIC_INFO_CHANGE;

                let confidence = if is_isolated { 0.8 } else { 0.5 };

                indicators.push(TimestompIndicator {
                    filename: event.filename.clone(),
                    mft_entry,
                    change_timestamp: event.timestamp,
                    has_nearby_data_change: false,
                    confidence,
                });
            }
        }
    }

    indicators
}

// ═══════════════════════════════════════════════════════════════════════════════
// Tests
// ═══════════════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod tests {
    use super::*;
    use chrono::{DateTime, Duration, Utc};
    use ntfs_core::usn::{FileAttributes, UsnReason, UsnRecord};

    /// Helper to build a synthetic `UsnRecord` for testing.
    fn make_record(
        mft_entry: u64,
        filename: &str,
        reason: UsnReason,
        timestamp: DateTime<Utc>,
        usn: i64,
    ) -> UsnRecord {
        UsnRecord {
            mft_entry,
            mft_sequence: 1,
            parent_mft_entry: 5,
            parent_mft_sequence: 1,
            usn,
            timestamp,
            reason,
            filename: filename.to_string(),
            file_attributes: FileAttributes::ARCHIVE,
            source_info: 0,
            security_id: 0,
            major_version: 2,
        }
    }

    fn ts(secs_offset: i64) -> DateTime<Utc> {
        DateTime::from_timestamp(1_700_000_000 + secs_offset, 0).unwrap()
    }

    // ─── Secure Deletion Tests ───────────────────────────────────────────

    #[test]
    fn test_detect_sdelete_pattern() {
        let records = vec![
            make_record(100, "AAAAAAA", UsnReason::FILE_CREATE, ts(0), 1000),
            make_record(100, "AAAAAAA", UsnReason::FILE_DELETE, ts(1), 1100),
            make_record(101, "ZZZZZZZ", UsnReason::FILE_CREATE, ts(2), 1200),
            make_record(101, "ZZZZZZZ", UsnReason::FILE_DELETE, ts(3), 1300),
            make_record(102, "0000000", UsnReason::FILE_CREATE, ts(4), 1400),
            make_record(102, "0000000", UsnReason::FILE_DELETE, ts(5), 1500),
        ];

        let indicators = detect_secure_deletion(&records);
        assert!(!indicators.is_empty());
        assert_eq!(indicators[0].pattern, SecureDeletionPattern::SDelete);
        assert!(indicators[0].confidence >= 0.9);
    }

    #[test]
    fn test_sdelete_not_triggered_by_normal_files() {
        let records = vec![
            make_record(100, "document.docx", UsnReason::FILE_CREATE, ts(0), 1000),
            make_record(101, "report.pdf", UsnReason::FILE_CREATE, ts(1), 1100),
            make_record(102, "image.png", UsnReason::FILE_DELETE, ts(2), 1200),
        ];

        let indicators = detect_secure_deletion(&records);
        assert!(indicators.is_empty());
    }

    #[test]
    fn test_detect_bulk_temp_deletion() {
        let mut records = Vec::new();
        for i in 0..15 {
            records.push(make_record(
                100 + i,
                &format!("tmp{i:04}.tmp"),
                UsnReason::FILE_DELETE,
                ts(i as i64),
                1000 + (i as i64) * 100,
            ));
        }

        let indicators = detect_secure_deletion(&records);
        assert!(!indicators.is_empty());
        assert_eq!(
            indicators[0].pattern,
            SecureDeletionPattern::BulkTempDeletion
        );
    }

    #[test]
    fn test_no_bulk_temp_with_few_files() {
        let records = vec![
            make_record(100, "tmp001.tmp", UsnReason::FILE_DELETE, ts(0), 1000),
            make_record(101, "tmp002.tmp", UsnReason::FILE_DELETE, ts(1), 1100),
        ];

        let indicators = detect_secure_deletion(&records);
        assert!(indicators.is_empty());
    }

    // ─── Journal Clearing Tests ──────────────────────────────────────────

    #[test]
    fn test_detect_high_starting_usn() {
        let records = vec![
            make_record(
                100,
                "file.txt",
                UsnReason::FILE_CREATE,
                ts(0),
                2_000_000_000, // 2GB - way above threshold
            ),
            make_record(
                101,
                "file2.txt",
                UsnReason::FILE_CREATE,
                ts(1),
                2_000_001_000,
            ),
        ];

        let result = detect_journal_clearing(&records);
        assert!(result.clearing_detected);
        assert!(result.confidence >= 0.4);
        assert_eq!(result.first_usn, Some(2_000_000_000));
    }

    #[test]
    fn test_detect_timestamp_gap() {
        let records = vec![
            make_record(100, "before.txt", UsnReason::FILE_CREATE, ts(0), 1000),
            // 48-hour gap
            make_record(
                101,
                "after.txt",
                UsnReason::FILE_CREATE,
                ts(48 * 3600),
                1100,
            ),
        ];

        let result = detect_journal_clearing(&records);
        assert!(!result.timestamp_gaps.is_empty());
        assert!(result.timestamp_gaps[0].gap_duration > Duration::hours(24));
    }

    #[test]
    fn test_no_clearing_for_normal_journal() {
        let records = vec![
            make_record(100, "a.txt", UsnReason::FILE_CREATE, ts(0), 100),
            make_record(101, "b.txt", UsnReason::FILE_CREATE, ts(60), 200),
            make_record(102, "c.txt", UsnReason::FILE_CREATE, ts(120), 300),
        ];

        let result = detect_journal_clearing(&records);
        assert!(!result.clearing_detected);
        assert!(result.timestamp_gaps.is_empty());
    }

    #[test]
    fn test_clearing_empty_records() {
        let result = detect_journal_clearing(&[]);
        assert!(!result.clearing_detected);
        assert!(result.first_usn.is_none());
    }

    // ─── Ransomware Detection Tests ──────────────────────────────────────

    #[test]
    fn test_detect_known_ransomware_extension() {
        let mut records = Vec::new();
        for i in 0..5 {
            records.push(make_record(
                100 + i,
                &format!("document{i}.docx.encrypted"),
                UsnReason::RENAME_NEW_NAME,
                ts(i as i64),
                1000 + (i as i64) * 100,
            ));
        }

        let indicators = detect_ransomware_patterns(&records);
        assert!(!indicators.is_empty());
        assert_eq!(indicators[0].extension, ".encrypted");
        assert_eq!(indicators[0].affected_count, 5);
    }

    #[test]
    fn test_detect_mass_rename_unknown_extension() {
        let mut records = Vec::new();
        for i in 0..25 {
            records.push(make_record(
                100 + i,
                &format!("file{i}.xyz_ransom"),
                UsnReason::RENAME_NEW_NAME,
                ts(i as i64),
                1000 + (i as i64) * 100,
            ));
        }

        let indicators = detect_ransomware_patterns(&records);
        assert!(!indicators.is_empty());
    }

    #[test]
    fn test_no_ransomware_for_normal_renames() {
        let records = vec![
            make_record(100, "doc1.docx", UsnReason::RENAME_NEW_NAME, ts(0), 1000),
            make_record(101, "image.png", UsnReason::RENAME_NEW_NAME, ts(100), 1100),
            make_record(102, "report.pdf", UsnReason::RENAME_NEW_NAME, ts(200), 1200),
        ];

        let indicators = detect_ransomware_patterns(&records);
        assert!(indicators.is_empty());
    }

    #[test]
    fn test_ransomware_multiple_known_extensions() {
        let mut records = Vec::new();
        // .locked files
        for i in 0..5 {
            records.push(make_record(
                100 + i,
                &format!("file{i}.locked"),
                UsnReason::RENAME_NEW_NAME,
                ts(i as i64),
                1000 + (i as i64) * 100,
            ));
        }
        // .crypto files
        for i in 0..4 {
            records.push(make_record(
                200 + i,
                &format!("photo{i}.crypto"),
                UsnReason::RENAME_NEW_NAME,
                ts(100 + i as i64),
                2000 + (i as i64) * 100,
            ));
        }

        let indicators = detect_ransomware_patterns(&records);
        // Should detect at least the .locked group (5 >= 3 threshold)
        let locked_indicators: Vec<_> = indicators
            .iter()
            .filter(|i| i.extension == ".locked")
            .collect();
        assert!(!locked_indicators.is_empty());
    }

    // ─── Timestomping Detection Tests ────────────────────────────────────

    #[test]
    fn test_detect_isolated_basic_info_change() {
        let records = vec![make_record(
            100,
            "suspicious.exe",
            UsnReason::BASIC_INFO_CHANGE,
            ts(1000),
            5000,
        )];

        let indicators = detect_timestomping(&records);
        assert!(!indicators.is_empty());
        assert_eq!(indicators[0].filename, "suspicious.exe");
        assert!(!indicators[0].has_nearby_data_change);
        assert!(indicators[0].confidence >= 0.7);
    }

    #[test]
    fn test_no_timestomp_with_data_change() {
        let records = vec![
            make_record(100, "normal.docx", UsnReason::DATA_OVERWRITE, ts(999), 4900),
            make_record(
                100,
                "normal.docx",
                UsnReason::BASIC_INFO_CHANGE,
                ts(1000),
                5000,
            ),
        ];

        let indicators = detect_timestomping(&records);
        assert!(indicators.is_empty());
    }

    #[test]
    fn test_timestomp_with_distant_data_change() {
        // Data change is > 60 seconds away, so BASIC_INFO_CHANGE is still suspicious
        let records = vec![
            make_record(
                100,
                "suspicious.exe",
                UsnReason::DATA_OVERWRITE,
                ts(0),
                1000,
            ),
            make_record(
                100,
                "suspicious.exe",
                UsnReason::BASIC_INFO_CHANGE,
                ts(120), // 2 minutes later
                5000,
            ),
        ];

        let indicators = detect_timestomping(&records);
        assert!(!indicators.is_empty());
    }

    #[test]
    fn test_timestomp_multiple_files() {
        let records = vec![
            make_record(
                100,
                "malware1.exe",
                UsnReason::BASIC_INFO_CHANGE,
                ts(0),
                1000,
            ),
            make_record(
                200,
                "malware2.dll",
                UsnReason::BASIC_INFO_CHANGE,
                ts(5),
                1500,
            ),
            make_record(300, "normal.txt", UsnReason::DATA_OVERWRITE, ts(10), 2000),
            make_record(
                300,
                "normal.txt",
                UsnReason::BASIC_INFO_CHANGE,
                ts(11),
                2100,
            ),
        ];

        let indicators = detect_timestomping(&records);
        // malware1.exe and malware2.dll should be flagged, normal.txt should not
        let flagged_files: Vec<&str> = indicators.iter().map(|i| i.filename.as_str()).collect();
        assert!(flagged_files.contains(&"malware1.exe"));
        assert!(flagged_files.contains(&"malware2.dll"));
        assert!(!flagged_files.contains(&"normal.txt"));
    }

    #[test]
    fn test_no_timestomp_on_create() {
        // FILE_CREATE is a legitimate reason for BASIC_INFO_CHANGE
        let records = vec![
            make_record(100, "newfile.txt", UsnReason::FILE_CREATE, ts(0), 1000),
            make_record(
                100,
                "newfile.txt",
                UsnReason::BASIC_INFO_CHANGE,
                ts(1),
                1100,
            ),
        ];

        let indicators = detect_timestomping(&records);
        assert!(indicators.is_empty());
    }

    // ─── Additional coverage tests ──────────────────────────────────────

    #[test]
    fn test_is_sdelete_filename_short() {
        assert!(!is_sdelete_filename("AB"));
        assert!(!is_sdelete_filename("A"));
        assert!(!is_sdelete_filename(""));
    }

    #[test]
    fn test_is_sdelete_filename_mixed_chars() {
        assert!(!is_sdelete_filename("ABCDEF"));
        assert!(!is_sdelete_filename("aaaaaa")); // lowercase not matched
    }

    #[test]
    fn test_is_sdelete_filename_with_extension() {
        assert!(is_sdelete_filename("AAAA.txt"));
        assert!(is_sdelete_filename("ZZZZZ.dat"));
        assert!(is_sdelete_filename("00000.bin"));
    }

    #[test]
    fn test_is_common_extension() {
        assert!(is_common_extension(".txt"));
        assert!(is_common_extension(".exe"));
        assert!(is_common_extension(".dll"));
        assert!(is_common_extension(".pdf"));
        assert!(!is_common_extension(".xyz_ransom"));
        assert!(!is_common_extension(".custom"));
    }

    #[test]
    fn test_sdelete_only_creates_lower_confidence() {
        // Only create events, no deletes -> lower confidence
        let records = vec![
            make_record(100, "AAAAAAA", UsnReason::FILE_CREATE, ts(0), 1000),
            make_record(101, "BBBBBBB", UsnReason::FILE_CREATE, ts(1), 1100),
            make_record(102, "CCCCCCC", UsnReason::FILE_CREATE, ts(2), 1200),
        ];

        let indicators = detect_secure_deletion(&records);
        assert!(!indicators.is_empty());
        assert!(indicators[0].confidence < 0.9);
    }

    #[test]
    fn test_sdelete_events_spread_over_time() {
        // Events more than 60 seconds apart should be separate groups
        let records = vec![
            make_record(100, "AAAAAAA", UsnReason::FILE_CREATE, ts(0), 1000),
            make_record(101, "AAAAAAA", UsnReason::FILE_DELETE, ts(1), 1100),
            // Gap > 60 seconds
            make_record(102, "BBBBBBB", UsnReason::FILE_CREATE, ts(120), 1200),
            make_record(103, "BBBBBBB", UsnReason::FILE_DELETE, ts(121), 1300),
        ];

        // Each pair has only 2 events, below the threshold of 3
        let indicators = detect_secure_deletion(&records);
        let sdelete_indicators: Vec<_> = indicators
            .iter()
            .filter(|i| i.pattern == SecureDeletionPattern::SDelete)
            .collect();
        assert!(sdelete_indicators.is_empty());
    }

    #[test]
    fn test_bulk_temp_deletion_spread_over_time() {
        // .tmp deletes more than 30 seconds apart should form separate groups
        let mut records = Vec::new();
        for i in 0..5 {
            records.push(make_record(
                100 + i,
                &format!("tmp{i:04}.tmp"),
                UsnReason::FILE_DELETE,
                ts(i as i64),
                1000 + (i as i64) * 100,
            ));
        }
        // Gap > 30 seconds
        for i in 0..5 {
            let n = 100 + i;
            records.push(make_record(
                200 + i,
                &format!("tmp{n:04}.tmp"),
                UsnReason::FILE_DELETE,
                ts(60 + i as i64),
                2000 + (i as i64) * 100,
            ));
        }

        let indicators = detect_secure_deletion(&records);
        let bulk = indicators
            .iter()
            .filter(|i| i.pattern == SecureDeletionPattern::BulkTempDeletion)
            .count();
        assert_eq!(bulk, 0);
    }

    #[test]
    fn test_mass_rename_with_common_extension_ignored() {
        // Mass renames to .txt should NOT trigger ransomware detection
        let mut records = Vec::new();
        for i in 0..25 {
            records.push(make_record(
                100 + i,
                &format!("file{i}.txt"),
                UsnReason::RENAME_NEW_NAME,
                ts(i as i64),
                1000 + (i as i64) * 100,
            ));
        }

        let indicators = detect_ransomware_patterns(&records);
        assert!(indicators.is_empty());
    }

    #[test]
    fn test_ransomware_high_count_high_confidence() {
        // 20+ renames to known extension should have 0.95 confidence
        let mut records = Vec::new();
        for i in 0..25 {
            records.push(make_record(
                100 + i,
                &format!("document{i}.docx.encrypted"),
                UsnReason::RENAME_NEW_NAME,
                ts(i as i64),
                1000 + (i as i64) * 100,
            ));
        }

        let indicators = detect_ransomware_patterns(&records);
        assert!(!indicators.is_empty());
        let encrypted_ind = indicators
            .iter()
            .find(|i| i.extension == ".encrypted")
            .unwrap();
        assert!(encrypted_ind.confidence >= 0.95);
    }

    #[test]
    fn test_ransomware_medium_count_medium_confidence() {
        // 10-19 renames should have 0.85 confidence
        let mut records = Vec::new();
        for i in 0..12 {
            records.push(make_record(
                100 + i,
                &format!("file{i}.locked"),
                UsnReason::RENAME_NEW_NAME,
                ts(i as i64),
                1000 + (i as i64) * 100,
            ));
        }

        let indicators = detect_ransomware_patterns(&records);
        let locked = indicators
            .iter()
            .find(|i| i.extension == ".locked")
            .unwrap();
        assert!((locked.confidence - 0.85).abs() < 0.01);
    }

    #[test]
    fn test_mass_rename_over_long_time_not_ransomware() {
        // Mass renames to same unknown extension but spread over > 10 minutes
        let mut records = Vec::new();
        for i in 0..25 {
            records.push(make_record(
                100 + i,
                &format!("file{i}.xyz_spread"),
                UsnReason::RENAME_NEW_NAME,
                ts(i as i64 * 60), // 1 minute apart, total > 10 min
                1000 + (i as i64) * 100,
            ));
        }

        let indicators = detect_ransomware_patterns(&records);
        let spread = indicators
            .iter()
            .filter(|i| i.extension == ".xyz_spread")
            .count();
        assert_eq!(spread, 0);
    }

    #[test]
    fn test_detect_journal_clearing_multiple_gaps() {
        // Multiple 24+ hour gaps increase confidence
        let records = vec![
            make_record(100, "a.txt", UsnReason::FILE_CREATE, ts(0), 100),
            make_record(101, "b.txt", UsnReason::FILE_CREATE, ts(25 * 3600), 200),
            make_record(102, "c.txt", UsnReason::FILE_CREATE, ts(50 * 3600), 300),
            make_record(103, "d.txt", UsnReason::FILE_CREATE, ts(75 * 3600), 400),
        ];

        let result = detect_journal_clearing(&records);
        assert_eq!(result.timestamp_gaps.len(), 3);
        assert!(result.confidence > 0.0);
    }

    #[test]
    fn test_timestomp_basic_info_change_with_close() {
        // BASIC_INFO_CHANGE | CLOSE (not isolated) should have lower confidence
        let records = vec![make_record(
            100,
            "stomped.exe",
            UsnReason::BASIC_INFO_CHANGE | UsnReason::CLOSE | UsnReason::SECURITY_CHANGE,
            ts(1000),
            5000,
        )];

        let indicators = detect_timestomping(&records);
        // A lone BASIC_INFO_CHANGE | CLOSE | SECURITY_CHANGE has no nearby data
        // change, so it is always flagged; the reason is not isolated (it carries
        // SECURITY_CHANGE), so confidence is the lower 0.5.
        assert!(!indicators.is_empty());
        assert!(indicators[0].confidence <= 0.5);
    }

    #[test]
    fn test_sdelete_grouping_splits_on_time_gap() {
        // Line 96: groups.push when a group of >= 3 SDelete events is followed
        // by a time gap > 60 seconds, then more events start a new group.
        let mut records = Vec::new();

        // First group: 4 SDelete events within 60 seconds
        for i in 0..4u64 {
            records.push(make_record(
                100 + i,
                &"A".repeat(7),
                UsnReason::FILE_CREATE,
                ts(i as i64),
                1000 + (i as i64) * 100,
            ));
        }

        // Time gap of 120 seconds (> 60s threshold)
        // Small group of 2 SDelete events (< 3, should be cleared not pushed)
        records.push(make_record(
            200,
            &"B".repeat(7),
            UsnReason::FILE_DELETE,
            ts(124),
            2000,
        ));
        records.push(make_record(
            201,
            &"B".repeat(7),
            UsnReason::FILE_DELETE,
            ts(125),
            2100,
        ));

        // Another time gap
        // Third group: 3 more SDelete events
        for i in 0..3u64 {
            records.push(make_record(
                300 + i,
                &"C".repeat(7),
                UsnReason::FILE_CREATE,
                ts(300 + i as i64),
                3000 + (i as i64) * 100,
            ));
        }

        let indicators = detect_secure_deletion(&records);
        // Should detect at least the first group (4 events) and third group (3 events)
        let sdelete_indicators: Vec<_> = indicators
            .iter()
            .filter(|i| i.pattern == SecureDeletionPattern::SDelete)
            .collect();
        assert!(!sdelete_indicators.is_empty());
    }

    #[test]
    fn test_bulk_temp_deletion_grouping_splits_on_time_gap() {
        // Line 173: groups.push when a group of >= 10 .tmp deletions is followed
        // by a time gap > 30 seconds, then more events.
        let mut records = Vec::new();

        // First group: 12 tmp file deletions within 30 seconds
        for i in 0..12u64 {
            records.push(make_record(
                100 + i,
                &format!("tmp{i:04}.tmp"),
                UsnReason::FILE_DELETE,
                ts(i as i64),
                1000 + (i as i64) * 100,
            ));
        }

        // Time gap of 60 seconds (> 30s threshold)
        // Small group of 5 tmp deletions (< 10, should be cleared)
        for i in 0..5u64 {
            records.push(make_record(
                200 + i,
                &format!("tmpB{i:04}.tmp"),
                UsnReason::FILE_DELETE,
                ts(72 + i as i64),
                2000 + (i as i64) * 100,
            ));
        }

        // Another time gap, then another group of 10
        for i in 0..10u64 {
            records.push(make_record(
                300 + i,
                &format!("tmpC{i:04}.tmp"),
                UsnReason::FILE_DELETE,
                ts(200 + i as i64),
                3000 + (i as i64) * 100,
            ));
        }

        let indicators = detect_secure_deletion(&records);
        let bulk_indicators: Vec<_> = indicators
            .iter()
            .filter(|i| i.pattern == SecureDeletionPattern::BulkTempDeletion)
            .collect();
        assert!(!bulk_indicators.is_empty());
    }

    #[test]
    fn test_timestomping_other_before_event() {
        // Line 532: other.timestamp - event.timestamp path
        // when other event happens BEFORE the BASIC_INFO_CHANGE event
        // This tests the branch where other.timestamp < event.timestamp
        // so the abs difference is event.timestamp - other.timestamp.
        // Wait, line 531-532 actually is:
        //   let time_diff = if other.timestamp >= event.timestamp {
        //       other.timestamp - event.timestamp    // line 532
        //   } else {
        //       event.timestamp - other.timestamp
        //   };
        // Line 532 fires when other.timestamp >= event.timestamp.
        // The existing tests have nearby data changes AFTER the BASIC_INFO_CHANGE.
        // We need a test where the data change is AT or AFTER the event.
        // Actually, we need an ISOLATED BASIC_INFO_CHANGE where nearby events
        // DON'T have data changes but DO have timestamps >= event.timestamp.

        let records = vec![
            // A BASIC_INFO_CHANGE event (possible timestomping)
            make_record(
                100,
                "stomped.exe",
                UsnReason::BASIC_INFO_CHANGE,
                ts(1000),
                5000,
            ),
            // A nearby event AFTER the BASIC_INFO_CHANGE (> its timestamp)
            // but with SECURITY_CHANGE (not a data change), so it doesn't
            // suppress the timestomping detection
            make_record(
                101,
                "other.txt",
                UsnReason::SECURITY_CHANGE,
                ts(1010), // 10 seconds after, within 60s window
                5100,
            ),
        ];

        let indicators = detect_timestomping(&records);
        assert!(!indicators.is_empty());
        assert_eq!(indicators[0].filename, "stomped.exe");
    }

    #[test]
    fn test_ransomware_renames_without_extension() {
        // Renames with no extension should not cause issues
        let mut records = Vec::new();
        for i in 0..25 {
            records.push(make_record(
                100 + i,
                &format!("file{i}"), // No extension
                UsnReason::RENAME_NEW_NAME,
                ts(i as i64),
                1000 + (i as i64) * 100,
            ));
        }

        let indicators = detect_ransomware_patterns(&records);
        // Should not crash, and no indicator for files without extensions
        assert!(indicators.is_empty());
    }

    #[test]
    fn test_timestomp_other_timestamp_after_event() {
        // Cover line 557: `other.timestamp - event.timestamp` branch.
        // Create a BASIC_INFO_CHANGE event with a DATA_OVERWRITE event that occurs
        // AFTER it (within 60 seconds). This exercises the `other.timestamp >= event.timestamp`
        // path where `time_diff = other.timestamp - event.timestamp`.
        let records = vec![
            make_record(100, "file.exe", UsnReason::BASIC_INFO_CHANGE, ts(100), 5000),
            make_record(
                100,
                "file.exe",
                UsnReason::DATA_OVERWRITE,
                ts(110), // 10 seconds AFTER the BASIC_INFO_CHANGE
                5100,
            ),
        ];

        let indicators = detect_timestomping(&records);
        // The DATA_OVERWRITE is within 60 seconds and after the BASIC_INFO_CHANGE,
        // so it should NOT be flagged as timestomping.
        assert!(indicators.is_empty());
    }

    #[test]
    fn test_known_ext_rename_without_matching_extension() {
        // detect_known_ransomware_extensions: a RENAME_NEW_NAME whose filename
        // ends with NONE of the known ransomware extensions exercises the path
        // where the inner `for ext` loop completes without `break`, letting
        // control fall through the end of the `if reason.contains(..)` block.
        let records = vec![
            make_record(100, "renamed.docx", UsnReason::RENAME_NEW_NAME, ts(0), 1000),
            make_record(101, "moved.pdf", UsnReason::RENAME_NEW_NAME, ts(1), 1100),
            make_record(102, "data.bin", UsnReason::RENAME_NEW_NAME, ts(2), 1200),
            // A non-rename record so the per-record `if reason.contains(RENAME_NEW_NAME)`
            // guard is skipped (its false arm), and a rename whose name matches no
            // ransomware extension so the inner `for ext` loop completes without break.
            make_record(103, "plain.txt", UsnReason::FILE_CREATE, ts(3), 1300),
        ];

        let indicators = detect_known_ransomware_extensions(&records);
        assert!(indicators.is_empty());
    }

    #[test]
    fn test_known_ext_group_below_threshold_skips() {
        // detect_known_ransomware_extensions: an extension group with fewer than
        // 3 members fails `group.len() >= 3` and falls through the `if` body,
        // reaching the loop-iteration tail of the per-extension loop.
        let records = vec![
            make_record(100, "a.locked", UsnReason::RENAME_NEW_NAME, ts(0), 1000),
            make_record(101, "b.locked", UsnReason::RENAME_NEW_NAME, ts(1), 1100),
        ];

        let indicators = detect_known_ransomware_extensions(&records);
        // Only 2 .locked renames — below the 3-member threshold.
        assert!(indicators.is_empty());
    }

    #[test]
    fn test_mass_rename_group_below_threshold_skips() {
        // detect_mass_rename_patterns: with 20+ total renames (passes the early
        // guard) but each unusual extension group having fewer than 20 members,
        // every group fails `group.len() >= 20` and falls through the `if` body.
        let mut records = Vec::new();
        for i in 0..25u64 {
            // Alternate between two distinct unusual extensions so neither group
            // reaches 20, while the overall rename count exceeds the 20 guard.
            let ext = if i % 2 == 0 { "aaa" } else { "bbb" };
            records.push(make_record(
                100 + i,
                &format!("file{i}.{ext}"),
                UsnReason::RENAME_NEW_NAME,
                ts(i as i64),
                1000 + (i as i64) * 100,
            ));
        }

        let indicators = detect_mass_rename_patterns(&records);
        // No single unusual-extension group reaches 20 members.
        assert!(indicators.is_empty());
    }

    #[test]
    fn test_timestomp_with_close_falls_through_when_empty() {
        // Companion to test_timestomp_basic_info_change_with_close: drive
        // detect_timestomping to an EMPTY result so the `if !indicators.is_empty()`
        // guard there evaluates false and falls through its closing brace.
        // A lone DATA_OVERWRITE (no BASIC_INFO_CHANGE) yields no indicators.
        let records = vec![make_record(
            100,
            "plain.bin",
            UsnReason::DATA_OVERWRITE,
            ts(1000),
            5000,
        )];

        let indicators = detect_timestomping(&records);
        assert!(indicators.is_empty());
    }
}
