//! Tests for crash recovery and log rotation integration.

use super::*;
use crate::format::{FileHeader, Record, HEADER_SIZE, MAX_RECORD_SIZE, RECORD_OVERHEAD};
use crate::journal::Journal;
use std::io::Write;
use tempfile::tempdir;

/// Helper: write a raw WAL file with header + records for testing
fn write_test_wal(path: &Path, records: &[&[u8]]) -> Vec<u64> {
    let mut file = File::create(path).expect("create file");
    let header = FileHeader::new();
    let mut buf = Vec::new();
    header.write_to(&mut buf).expect("write header");
    file.write_all(&buf).expect("write header bytes");

    let mut offsets = Vec::new();
    let mut offset = HEADER_SIZE as u64;

    for payload in records {
        offsets.push(offset);
        let record = Record::new(payload.to_vec());
        let mut rec_buf = Vec::new();
        record.write_to(&mut rec_buf).expect("write record");
        file.write_all(&rec_buf).expect("write record bytes");
        offset += record.disk_size();
    }

    file.sync_all().expect("sync");
    offsets
}

#[test]
fn test_recovery_truncates_partial_record() {
    let dir = tempdir().expect("tempdir");
    let wal_file = dir.path().join("test.wal");

    write_test_wal(&wal_file, &[b"record 0", b"record 1", b"record 2"]);

    // Append 5 trailing junk bytes
    let mut file = OpenOptions::new().append(true).open(&wal_file).expect("open");
    file.write_all(&[0xDE, 0xAD, 0xBE, 0xEF, 0x42]).expect("write junk");
    file.sync_all().expect("sync");

    let report = recover_file(&wal_file).expect("recover");
    assert_eq!(report.valid_records, 3);
    assert_eq!(report.bytes_truncated, 5);
    assert_eq!(report.invalid_records, 1);
}

#[test]
fn test_recovery_truncates_corrupt_checksum() {
    let dir = tempdir().expect("tempdir");
    let wal_file = dir.path().join("test.wal");

    write_test_wal(&wal_file, &[b"record 0", b"record 1", b"record 2"]);

    // Corrupt a byte in record 2's payload area
    let mut data = std::fs::read(&wal_file).expect("read file");
    let corrupt_pos = data.len() - 2;
    data[corrupt_pos] ^= 0xFF;
    std::fs::write(&wal_file, &data).expect("write corrupted");

    let report = recover_file(&wal_file).expect("recover");
    assert_eq!(report.valid_records, 2);
    assert_eq!(report.invalid_records, 1);
    assert!(report.bytes_truncated > 0);
}

#[test]
fn test_recovery_handles_empty_after_header() {
    let dir = tempdir().expect("tempdir");
    let wal_file = dir.path().join("test.wal");

    // Write just a header
    let mut file = File::create(&wal_file).expect("create");
    let header = FileHeader::new();
    let mut buf = Vec::new();
    header.write_to(&mut buf).expect("write header");
    file.write_all(&buf).expect("write");
    file.sync_all().expect("sync");

    let report = recover_file(&wal_file).expect("recover");
    assert_eq!(report.valid_records, 0);
    assert_eq!(report.bytes_truncated, 0);
    assert_eq!(report.final_offset, HEADER_SIZE as u64);
}

#[test]
fn test_recovery_handles_truncated_header() {
    let dir = tempdir().expect("tempdir");
    let wal_file = dir.path().join("test.wal");

    // Write only 4 bytes (less than HEADER_SIZE = 8)
    std::fs::write(&wal_file, [0x53, 0x54, 0x45, 0x4D]).expect("write");

    let report = recover_file(&wal_file).expect("recover");
    assert_eq!(report.valid_records, 0);
    assert_eq!(report.bytes_truncated, 4);
    assert_eq!(report.final_offset, 0);
}

#[test]
fn test_recovery_report_metrics() {
    let dir = tempdir().expect("tempdir");
    let wal_file = dir.path().join("test.wal");

    write_test_wal(&wal_file, &[b"alpha", b"beta", b"gamma"]);

    let report = recover_file(&wal_file).expect("recover");
    assert_eq!(report.valid_records, 3);
    assert_eq!(report.invalid_records, 0);
    assert_eq!(report.bytes_truncated, 0);
    assert!(report.recovery_duration < Duration::from_secs(5));

    let expected_offset = HEADER_SIZE as u64 + 3 * RECORD_OVERHEAD as u64 + 5 + 4 + 5; // alpha=5, beta=4, gamma=5
    assert_eq!(report.final_offset, expected_offset);
}

#[test]
fn test_recovery_preserves_valid_before_corruption() {
    let dir = tempdir().expect("tempdir");
    let wal_file = dir.path().join("test.wal");

    let offsets = write_test_wal(&wal_file, &[b"keep me", b"keep me too", b"corrupt me"]);

    // Corrupt record 2 by flipping a CRC byte (bytes 4..8 of that record)
    let mut data = std::fs::read(&wal_file).expect("read");
    let record2_crc_offset = offsets[2] as usize + 4; // skip payload_len, hit CRC
    data[record2_crc_offset] ^= 0xFF;
    std::fs::write(&wal_file, &data).expect("write");

    let report = recover_file(&wal_file).expect("recover");
    assert_eq!(report.valid_records, 2);
    assert_eq!(report.final_offset, offsets[2]);

    // Verify the file was actually truncated
    let new_len = std::fs::metadata(&wal_file).expect("metadata").len();
    assert_eq!(new_len, offsets[2]);
}

#[test]
fn test_recovery_handles_zero_length_record() {
    let dir = tempdir().expect("tempdir");
    let wal_file = dir.path().join("test.wal");

    write_test_wal(&wal_file, &[b"good record"]);

    // Append a record with payload_len = 0 (invalid)
    let mut file = OpenOptions::new().append(true).open(&wal_file).expect("open");
    file.write_all(&0u32.to_le_bytes()).expect("write zero len");
    file.write_all(&[0u8; 36]).expect("write padding"); // crc + blake3
    file.sync_all().expect("sync");

    let report = recover_file(&wal_file).expect("recover");
    assert_eq!(report.valid_records, 1);
    assert_eq!(report.invalid_records, 1);
    assert!(report.bytes_truncated > 0);
}

#[test]
fn test_recovery_handles_impossibly_large_length() {
    let dir = tempdir().expect("tempdir");
    let wal_file = dir.path().join("test.wal");

    write_test_wal(&wal_file, &[b"good record"]);

    // Append a record with payload_len = MAX + 1 (invalid)
    let huge_len = (MAX_RECORD_SIZE as u32) + 1;
    let mut file = OpenOptions::new().append(true).open(&wal_file).expect("open");
    file.write_all(&huge_len.to_le_bytes()).expect("write huge len");
    file.sync_all().expect("sync");

    let report = recover_file(&wal_file).expect("recover");
    assert_eq!(report.valid_records, 1);
    assert_eq!(report.invalid_records, 1);
}

/// Integration test: Journal uses recover_file under the hood
#[test]
fn test_journal_recovery_integration() {
    let dir = tempdir().expect("tempdir");
    let wal_path = dir.path().join("wal");

    let offsets: Vec<u64>;

    // Write records via Journal
    {
        let mut journal = Journal::open(&wal_path).expect("open journal");
        offsets = (0..5)
            .map(|i| journal.append(format!("record {}", i).into_bytes()).expect("append"))
            .collect();
    }

    // Append junk to simulate torn write
    let wal_file = wal_path.join("0000000000000000.wal");
    let mut file = OpenOptions::new().append(true).open(&wal_file).expect("open");
    file.write_all(&[0xFF; 20]).expect("write junk");
    file.sync_all().expect("sync");

    // Journal should recover cleanly
    {
        let mut journal = Journal::open(&wal_path).expect("reopen journal");
        for (i, offset) in offsets.iter().enumerate() {
            let record = journal.read(*offset).expect("read record");
            assert_eq!(record.payload, format!("record {}", i).into_bytes());
        }
    }
}

/// Performance: recovery of 10K records should be fast
#[test]
fn test_recovery_performance_10k_records() {
    let dir = tempdir().expect("tempdir");
    let wal_file = dir.path().join("test.wal");

    let payloads: Vec<&[u8]> =
        (0..10_000).map(|_| b"benchmark payload data here" as &[u8]).collect();
    write_test_wal(&wal_file, &payloads);

    // Corrupt the last record
    let mut data = std::fs::read(&wal_file).expect("read");
    let last = data.len() - 1;
    data[last] ^= 0xFF;
    std::fs::write(&wal_file, &data).expect("write");

    let report = recover_file(&wal_file).expect("recover");
    assert_eq!(report.valid_records, 9_999);
    assert!(
        report.recovery_duration < Duration::from_secs(10),
        "Recovery took {:?}",
        report.recovery_duration
    );
}

// =========================================================================
// Wave 4: Log Rotation Integration Tests
// =========================================================================

/// Test: Rotation creates new segments at the configured threshold.
#[test]
fn test_rotation_creates_new_segment() {
    let dir = tempdir().expect("tempdir");
    let wal_path = dir.path().join("wal");

    // Use a tiny max_segment_size (1KB) to trigger rotation quickly
    let mut journal = Journal::open_with_max_segment_size(&wal_path, 1024).expect("open journal");

    let mut offsets = Vec::new();
    // Write enough records to trigger multiple rotations
    for i in 0..50 {
        let payload = format!("rotation test record {} with some padding data", i).into_bytes();
        let offset = journal.append(payload).expect("append");
        offsets.push(offset);
    }

    // Verify we created multiple segments
    let segment_files: Vec<_> = std::fs::read_dir(&wal_path)
        .expect("readdir")
        .filter_map(|e| e.ok())
        .filter(|e| e.path().extension().map(|ext| ext == "wal").unwrap_or(false))
        .collect();
    assert!(segment_files.len() > 1, "Expected multiple segments, got {}", segment_files.len());
}

/// Test: Records can be read across segment boundaries.
#[test]
fn test_read_across_segments() {
    let dir = tempdir().expect("tempdir");
    let wal_path = dir.path().join("wal");

    // 512 byte threshold to force rotation
    let mut journal = Journal::open_with_max_segment_size(&wal_path, 512).expect("open journal");

    let mut records = Vec::new();
    for i in 0..30 {
        let payload = format!("cross-segment record {}", i).into_bytes();
        let offset = journal.append(payload.clone()).expect("append");
        records.push((offset, payload));
    }

    // Read all records back - some will span segment boundaries
    for (offset, expected_payload) in &records {
        let record = journal.read(*offset).expect("read across segments");
        assert_eq!(&record.payload, expected_payload);
    }
}

/// Test: Recovery works across multiple segments.
#[test]
fn test_recovery_across_segments() {
    let dir = tempdir().expect("tempdir");
    let wal_path = dir.path().join("wal");

    let mut records = Vec::new();

    // Write records with small segments
    {
        let mut journal = Journal::open_with_max_segment_size(&wal_path, 512).expect("open");
        for i in 0..20 {
            let payload = format!("recovery segment test {}", i).into_bytes();
            let offset = journal.append(payload.clone()).expect("append");
            records.push((offset, payload));
        }
    }

    // Append junk to the last segment to simulate torn write
    let last_segment = std::fs::read_dir(&wal_path)
        .expect("readdir")
        .filter_map(|e| e.ok())
        .filter(|e| e.path().extension().map(|ext| ext == "wal").unwrap_or(false))
        .max_by_key(|e| e.file_name())
        .expect("at least one segment");
    let mut file =
        OpenOptions::new().append(true).open(last_segment.path()).expect("open last segment");
    file.write_all(&[0xDE, 0xAD, 0xBE, 0xEF]).expect("write junk");
    file.sync_all().expect("sync");

    // Recovery should preserve all valid records
    {
        let mut journal = Journal::open_with_max_segment_size(&wal_path, 512).expect("reopen");
        for (offset, expected) in &records {
            let record = journal.read(*offset).expect("read after recovery");
            assert_eq!(&record.payload, expected);
        }
    }
}

/// Test: Appending after recovery with rotation works.
#[test]
fn test_append_after_recovery_with_rotation() {
    let dir = tempdir().expect("tempdir");
    let wal_path = dir.path().join("wal");

    let mut pre_recovery_records = Vec::new();

    // Phase 1: Write some records
    {
        let mut journal = Journal::open_with_max_segment_size(&wal_path, 512).expect("open");
        for i in 0..10 {
            let payload = format!("before recovery {}", i).into_bytes();
            let offset = journal.append(payload.clone()).expect("append");
            pre_recovery_records.push((offset, payload));
        }
    }

    // Phase 2: Recover and continue writing
    let mut post_recovery_records = Vec::new();
    {
        let mut journal = Journal::open_with_max_segment_size(&wal_path, 512).expect("reopen");

        // Verify pre-recovery records
        for (offset, expected) in &pre_recovery_records {
            let record = journal.read(*offset).expect("read pre-recovery");
            assert_eq!(&record.payload, expected);
        }

        // Write more records
        for i in 0..10 {
            let payload = format!("after recovery {}", i).into_bytes();
            let offset = journal.append(payload.clone()).expect("append");
            post_recovery_records.push((offset, payload));
        }
    }

    // Phase 3: Final verification
    {
        let mut journal = Journal::open_with_max_segment_size(&wal_path, 512).expect("final open");
        for (offset, expected) in pre_recovery_records.iter().chain(&post_recovery_records) {
            let record = journal.read(*offset).expect("read final");
            assert_eq!(&record.payload, expected);
        }
    }
}

/// Test: Cleanup removes old segments after cursor advances.
#[test]
fn test_cleanup_removes_old_segments() {
    let dir = tempdir().expect("tempdir");
    let wal_path = dir.path().join("wal");

    let mut journal = Journal::open_with_max_segment_size(&wal_path, 512).expect("open");

    // Write enough to create multiple segments
    let mut last_offset = 0;
    for i in 0..30 {
        let payload = format!("cleanup test record {}", i).into_bytes();
        last_offset = journal.append(payload).expect("append");
    }

    let count_segments = || -> usize {
        std::fs::read_dir(&wal_path)
            .expect("readdir")
            .filter_map(|e| e.ok())
            .filter(|e| e.path().extension().map(|ext| ext == "wal").unwrap_or(false))
            .count()
    };

    let initial_segments = count_segments();
    assert!(initial_segments > 1, "Should have multiple segments");

    // Cleanup with cursor at the last offset should remove old segments
    let freed = journal.cleanup(last_offset).expect("cleanup");
    assert!(freed > 0, "Should have freed some bytes");

    let final_segments = count_segments();
    assert!(
        final_segments < initial_segments,
        "Should have fewer segments after cleanup: {} -> {}",
        initial_segments,
        final_segments
    );
}
