//! Integration tests for database persistence and recovery.
//!
//! These tests verify that data persists across database close/reopen cycles.

use dig_l2_storage::{BlockHeader, BlockStorage, DataLocation, Emission, StoredBlock};
use tempfile::TempDir;

/// Helper to create a test block.
fn create_test_block(epoch: u64, height: u64, proposer: &[u8]) -> StoredBlock {
    StoredBlock {
        header: BlockHeader {
            epoch,
            height,
            prev_block_root: [0u8; 32],
            timestamp: 1700000000 + height,
            proposer_pubkey: proposer.to_vec(),
            extra_data: [0u8; 32],
        },
        data_hashes: vec![[1u8; 32], [2u8; 32]],
        emissions: vec![Emission {
            pubkey: proposer.to_vec(),
            weight: 100,
        }],
        data_root: [3u8; 32],
        emissions_root: [4u8; 32],
    }
}

fn make_root(epoch: u64, height: u64) -> [u8; 32] {
    let mut root = [0u8; 32];
    root[0..8].copy_from_slice(&epoch.to_le_bytes());
    root[8..16].copy_from_slice(&height.to_le_bytes());
    root
}

/// Test that blocks persist after closing and reopening the database.
#[test]
fn test_block_persistence_after_reopen() {
    let tmp = TempDir::new().unwrap();
    let db_path = tmp.path().to_path_buf();

    // First session: write blocks
    {
        let storage = BlockStorage::open(&db_path).unwrap();

        for epoch in 0..5u64 {
            for height in 0..3u64 {
                let root = make_root(epoch, height);
                let block = create_test_block(epoch, height, &[epoch as u8, height as u8]);
                storage.put_block(epoch, &root, &block).unwrap();
            }
        }

        storage.flush().unwrap();
        // Storage drops here, closing the database
    }

    // Second session: verify blocks still exist
    {
        let storage = BlockStorage::open(&db_path).unwrap();

        // Verify block count
        let count = storage.block_count().unwrap();
        assert_eq!(count, 15, "Should have 15 blocks after reopen");

        // Verify specific blocks
        for epoch in 0..5u64 {
            for height in 0..3u64 {
                let root = make_root(epoch, height);
                assert!(
                    storage.has_block(epoch, &root).unwrap(),
                    "Block ({}, {}) should exist after reopen",
                    epoch,
                    height
                );

                let block = storage.get_block(epoch, &root).unwrap().expect("Block should be found");
                assert_eq!(block.header.epoch, epoch);
                assert_eq!(block.header.height, height);
            }
        }
    }
}

/// Test that canonical chain persists.
#[test]
fn test_canonical_persistence() {
    let tmp = TempDir::new().unwrap();
    let db_path = tmp.path().to_path_buf();

    // First session: set canonical roots
    {
        let storage = BlockStorage::open(&db_path).unwrap();

        for epoch in 0..10u64 {
            let root = make_root(epoch, epoch * 100);
            storage.set_canonical(epoch, &root).unwrap();
        }

        storage.flush().unwrap();
    }

    // Second session: verify canonical roots
    {
        let storage = BlockStorage::open(&db_path).unwrap();

        for epoch in 0..10u64 {
            let expected = make_root(epoch, epoch * 100);
            let actual = storage.get_canonical(epoch).unwrap().expect("Canonical should persist");
            assert_eq!(actual, expected, "Canonical for epoch {} should match", epoch);
        }
    }
}

/// Test that state persists.
#[test]
fn test_state_persistence() {
    let tmp = TempDir::new().unwrap();
    let db_path = tmp.path().to_path_buf();

    // First session: write state
    {
        let storage = BlockStorage::open(&db_path).unwrap();

        storage.set_last_height(987654).unwrap();
        storage.put_state("custom_key", b"custom_value").unwrap();
        storage.put_state("network_id", b"mainnet").unwrap();

        storage.flush().unwrap();
    }

    // Second session: verify state
    {
        let storage = BlockStorage::open(&db_path).unwrap();

        assert_eq!(storage.get_last_height().unwrap(), Some(987654));
        assert_eq!(
            storage.get_state("custom_key").unwrap(),
            Some(b"custom_value".to_vec())
        );
        assert_eq!(
            storage.get_state("network_id").unwrap(),
            Some(b"mainnet".to_vec())
        );
    }
}

/// Test that data index persists.
#[test]
fn test_data_index_persistence() {
    let tmp = TempDir::new().unwrap();
    let db_path = tmp.path().to_path_buf();

    // First session: index data
    {
        let storage = BlockStorage::open(&db_path).unwrap();

        for i in 0..20u64 {
            let mut hash = [0u8; 32];
            hash[0..8].copy_from_slice(&i.to_le_bytes());

            let location = DataLocation {
                epoch: i / 5,
                block_root: make_root(i / 5, i),
                index: i as usize,
            };

            storage.index_data(&hash, &location).unwrap();
        }

        storage.flush().unwrap();
    }

    // Second session: verify index
    {
        let storage = BlockStorage::open(&db_path).unwrap();

        for i in 0..20u64 {
            let mut hash = [0u8; 32];
            hash[0..8].copy_from_slice(&i.to_le_bytes());

            let location = storage.find_data(&hash).unwrap().expect("Data should persist");
            assert_eq!(location.epoch, i / 5);
            assert_eq!(location.index, i as usize);
        }
    }
}

/// Test multiple reopen cycles.
#[test]
fn test_multiple_reopen_cycles() {
    let tmp = TempDir::new().unwrap();
    let db_path = tmp.path().to_path_buf();

    // Cycle 1: Create initial data
    {
        let storage = BlockStorage::open(&db_path).unwrap();
        let root = make_root(0, 0);
        let block = create_test_block(0, 0, &[0]);
        storage.put_block(0, &root, &block).unwrap();
        storage.set_last_height(100).unwrap();
        storage.flush().unwrap();
    }

    // Cycle 2: Add more data
    {
        let storage = BlockStorage::open(&db_path).unwrap();

        // Verify existing data
        assert_eq!(storage.block_count().unwrap(), 1);
        assert_eq!(storage.get_last_height().unwrap(), Some(100));

        // Add more
        let root = make_root(0, 1);
        let block = create_test_block(0, 1, &[1]);
        storage.put_block(0, &root, &block).unwrap();
        storage.set_last_height(200).unwrap();
        storage.flush().unwrap();
    }

    // Cycle 3: Add even more data
    {
        let storage = BlockStorage::open(&db_path).unwrap();

        // Verify all data so far
        assert_eq!(storage.block_count().unwrap(), 2);
        assert_eq!(storage.get_last_height().unwrap(), Some(200));

        // Add more
        let root = make_root(1, 0);
        let block = create_test_block(1, 0, &[2]);
        storage.put_block(1, &root, &block).unwrap();
        storage.set_last_height(300).unwrap();
        storage.flush().unwrap();
    }

    // Final verification
    {
        let storage = BlockStorage::open(&db_path).unwrap();
        assert_eq!(storage.block_count().unwrap(), 3);
        assert_eq!(storage.get_last_height().unwrap(), Some(300));

        // Verify specific blocks
        assert!(storage.has_block(0, &make_root(0, 0)).unwrap());
        assert!(storage.has_block(0, &make_root(0, 1)).unwrap());
        assert!(storage.has_block(1, &make_root(1, 0)).unwrap());
    }
}

/// Test that updates to existing keys persist.
#[test]
fn test_update_persistence() {
    let tmp = TempDir::new().unwrap();
    let db_path = tmp.path().to_path_buf();

    let epoch = 5;
    let root = make_root(epoch, 100);

    // First session: write initial block
    {
        let storage = BlockStorage::open(&db_path).unwrap();
        let block = create_test_block(epoch, 100, &[0xAA]);
        storage.put_block(epoch, &root, &block).unwrap();
        storage.flush().unwrap();
    }

    // Second session: update the block
    {
        let storage = BlockStorage::open(&db_path).unwrap();

        // Verify original
        let original = storage.get_block(epoch, &root).unwrap().unwrap();
        assert_eq!(original.header.proposer_pubkey, vec![0xAA]);

        // Update with new block
        let updated_block = create_test_block(epoch, 100, &[0xBB, 0xCC]);
        storage.put_block(epoch, &root, &updated_block).unwrap();
        storage.flush().unwrap();
    }

    // Third session: verify update persisted
    {
        let storage = BlockStorage::open(&db_path).unwrap();
        let block = storage.get_block(epoch, &root).unwrap().unwrap();
        assert_eq!(block.header.proposer_pubkey, vec![0xBB, 0xCC]);
    }
}

/// Test database with large dataset persistence.
#[test]
fn test_large_dataset_persistence() {
    let tmp = TempDir::new().unwrap();
    let db_path = tmp.path().to_path_buf();

    let num_epochs = 10u64;
    let blocks_per_epoch = 50u64;
    let total_blocks = num_epochs * blocks_per_epoch;

    // First session: write large dataset
    {
        let storage = BlockStorage::open(&db_path).unwrap();

        for epoch in 0..num_epochs {
            for height in 0..blocks_per_epoch {
                let root = make_root(epoch, height);
                let block = create_test_block(epoch, height, &[epoch as u8, height as u8]);
                storage.put_block(epoch, &root, &block).unwrap();
            }
            storage.set_canonical(epoch, &make_root(epoch, blocks_per_epoch - 1)).unwrap();
        }

        storage.flush().unwrap();
    }

    // Second session: verify large dataset
    {
        let storage = BlockStorage::open(&db_path).unwrap();

        // Verify total count
        let count = storage.block_count().unwrap();
        assert_eq!(count, total_blocks as usize, "Total block count should match");

        // Verify per-epoch counts
        for epoch in 0..num_epochs {
            let roots = storage.get_epoch_blocks(epoch).unwrap();
            assert_eq!(
                roots.len(),
                blocks_per_epoch as usize,
                "Epoch {} should have correct block count",
                epoch
            );
        }

        // Verify canonical
        for epoch in 0..num_epochs {
            let canonical = storage.get_canonical(epoch).unwrap().expect("Canonical should exist");
            let expected = make_root(epoch, blocks_per_epoch - 1);
            assert_eq!(canonical, expected);
        }
    }
}
