//! Integration tests for block storage operations.
//!
//! These tests use a live RocksDB database to verify all storage operations.

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

/// Helper to create a test block with configurable parameters.
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], [3u8; 32]],
        emissions: vec![
            Emission {
                pubkey: proposer.to_vec(),
                weight: 100,
            },
            Emission {
                pubkey: vec![0xAB, 0xCD],
                weight: 50,
            },
        ],
        data_root: [4u8; 32],
        emissions_root: [5u8; 32],
    }
}

/// Helper to generate a unique block root from epoch and height.
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 opening and creating a new database.
#[test]
fn test_open_new_database() {
    let tmp = TempDir::new().unwrap();
    let storage = BlockStorage::open(tmp.path()).expect("Failed to open new database");

    // New database should have zero blocks
    let count = storage.block_count().expect("Failed to count blocks");
    assert_eq!(count, 0, "New database should have no blocks");
}

/// Test storing and retrieving a single block.
#[test]
fn test_store_and_retrieve_block() {
    let tmp = TempDir::new().unwrap();
    let storage = BlockStorage::open(tmp.path()).unwrap();

    let epoch = 1;
    let root = make_root(epoch, 100);
    let block = create_test_block(epoch, 100, &[1, 2, 3, 4]);

    // Store block
    storage.put_block(epoch, &root, &block).expect("Failed to store block");

    // Verify it exists
    assert!(storage.has_block(epoch, &root).unwrap(), "Block should exist");

    // Retrieve and verify contents
    let retrieved = storage.get_block(epoch, &root).unwrap().expect("Block should be found");
    assert_eq!(retrieved.header.epoch, epoch);
    assert_eq!(retrieved.header.height, 100);
    assert_eq!(retrieved.header.proposer_pubkey, vec![1, 2, 3, 4]);
    assert_eq!(retrieved.data_hashes.len(), 3);
    assert_eq!(retrieved.emissions.len(), 2);
}

/// Test storing raw block data.
#[test]
fn test_store_raw_block_data() {
    let tmp = TempDir::new().unwrap();
    let storage = BlockStorage::open(tmp.path()).unwrap();

    let epoch = 5;
    let root = make_root(epoch, 500);
    let raw_data = b"This is raw block data for testing purposes";

    // Store raw
    storage.store_block(epoch, &root, raw_data).expect("Failed to store raw block");

    // Verify exists
    assert!(storage.has_block(epoch, &root).unwrap());

    // Retrieve raw
    let retrieved = storage.get_block_raw(epoch, &root).unwrap().expect("Raw block should exist");
    assert_eq!(&retrieved[..], raw_data);
}

/// Test block serialization round-trip.
#[test]
fn test_block_serialization_roundtrip() {
    let block = create_test_block(10, 1000, &[0xDE, 0xAD, 0xBE, 0xEF]);

    let bytes = block.to_bytes().expect("Failed to serialize block");
    let deserialized = StoredBlock::from_bytes(&bytes).expect("Failed to deserialize block");

    assert_eq!(deserialized.header.epoch, 10);
    assert_eq!(deserialized.header.height, 1000);
    assert_eq!(deserialized.header.proposer_pubkey, vec![0xDE, 0xAD, 0xBE, 0xEF]);
    assert_eq!(deserialized.data_hashes, block.data_hashes);
    assert_eq!(deserialized.emissions.len(), block.emissions.len());
}

/// Test storing multiple blocks in the same epoch.
#[test]
fn test_multiple_blocks_same_epoch() {
    let tmp = TempDir::new().unwrap();
    let storage = BlockStorage::open(tmp.path()).unwrap();

    let epoch = 3;
    let block_count = 10;

    // Store multiple blocks
    for i in 0..block_count {
        let root = make_root(epoch, i);
        let block = create_test_block(epoch, i, &[i as u8]);
        storage.put_block(epoch, &root, &block).unwrap();
    }

    // Verify all blocks exist
    for i in 0..block_count {
        let root = make_root(epoch, i);
        assert!(storage.has_block(epoch, &root).unwrap(), "Block {} should exist", i);
    }

    // Get all blocks for epoch
    let roots = storage.get_epoch_blocks(epoch).unwrap();
    assert_eq!(roots.len(), block_count as usize, "Should have all blocks for epoch");
}

/// Test storing blocks across multiple epochs.
#[test]
fn test_blocks_multiple_epochs() {
    let tmp = TempDir::new().unwrap();
    let storage = BlockStorage::open(tmp.path()).unwrap();

    let epochs = 5u64;
    let blocks_per_epoch = 3u64;

    // Store blocks across epochs
    for epoch in 0..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();
        }
    }

    // Verify block count
    let total = storage.block_count().unwrap();
    assert_eq!(total, (epochs * blocks_per_epoch) as usize);

    // Verify each epoch
    for epoch in 0..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);
    }
}

/// Test that non-existent blocks return None.
#[test]
fn test_get_nonexistent_block() {
    let tmp = TempDir::new().unwrap();
    let storage = BlockStorage::open(tmp.path()).unwrap();

    let root = [0xFF; 32];

    assert!(!storage.has_block(999, &root).unwrap(), "Non-existent block should not exist");
    assert!(storage.get_block(999, &root).unwrap().is_none(), "Get should return None");
    assert!(storage.get_block_raw(999, &root).unwrap().is_none(), "Get raw should return None");
}

/// Test canonical chain operations.
#[test]
fn test_canonical_chain() {
    let tmp = TempDir::new().unwrap();
    let storage = BlockStorage::open(tmp.path()).unwrap();

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

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

    // Non-existent canonical
    assert!(storage.get_canonical(999).unwrap().is_none(), "Non-existent canonical should be None");
}

/// Test updating canonical chain.
#[test]
fn test_update_canonical() {
    let tmp = TempDir::new().unwrap();
    let storage = BlockStorage::open(tmp.path()).unwrap();

    let epoch = 5;
    let root1 = [0xAA; 32];
    let root2 = [0xBB; 32];

    // Set initial canonical
    storage.set_canonical(epoch, &root1).unwrap();
    assert_eq!(storage.get_canonical(epoch).unwrap().unwrap(), root1);

    // Update canonical (reorg simulation)
    storage.set_canonical(epoch, &root2).unwrap();
    assert_eq!(storage.get_canonical(epoch).unwrap().unwrap(), root2);
}

/// Test data indexing.
#[test]
fn test_data_index() {
    let tmp = TempDir::new().unwrap();
    let storage = BlockStorage::open(tmp.path()).unwrap();

    // Index multiple data items
    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 % 5),
            index: i as usize,
        };

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

    // Find each data item
    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 be indexed");
        assert_eq!(location.epoch, i / 5);
        assert_eq!(location.index, i as usize);
    }

    // Non-existent data
    let missing = [0xFF; 32];
    assert!(storage.find_data(&missing).unwrap().is_none());
}

/// Test state storage.
#[test]
fn test_state_storage() {
    let tmp = TempDir::new().unwrap();
    let storage = BlockStorage::open(tmp.path()).unwrap();

    // Test arbitrary state
    let key = "my_custom_state";
    let value = b"custom state value with special chars: \x00\xFF";

    storage.put_state(key, value).unwrap();
    let retrieved = storage.get_state(key).unwrap().expect("State should exist");
    assert_eq!(&retrieved[..], value);

    // Test last height
    storage.set_last_height(12345).unwrap();
    assert_eq!(storage.get_last_height().unwrap(), Some(12345));

    storage.set_last_height(99999).unwrap();
    assert_eq!(storage.get_last_height().unwrap(), Some(99999));
}

/// Test state storage for various key types.
#[test]
fn test_state_storage_various_keys() {
    let tmp = TempDir::new().unwrap();
    let storage = BlockStorage::open(tmp.path()).unwrap();

    let test_cases = vec![
        ("simple", b"simple_value".to_vec()),
        ("with/slashes/path", b"path_value".to_vec()),
        ("with.dots.key", b"dots_value".to_vec()),
        ("unicode_key_\u{1F600}", b"emoji_key_value".to_vec()),
        ("empty_value", vec![]),
        ("binary_value", vec![0x00, 0x01, 0xFF, 0xFE]),
    ];

    for (key, value) in &test_cases {
        storage.put_state(key, value).unwrap();
    }

    for (key, expected_value) in &test_cases {
        let retrieved = storage.get_state(key).unwrap().expect("State should exist");
        assert_eq!(&retrieved, expected_value, "Value for key '{}' should match", key);
    }
}

/// Test flush operation.
#[test]
fn test_flush() {
    let tmp = TempDir::new().unwrap();
    let storage = BlockStorage::open(tmp.path()).unwrap();

    // Write some data
    let root = make_root(1, 1);
    let block = create_test_block(1, 1, &[1, 2, 3]);
    storage.put_block(1, &root, &block).unwrap();

    // Flush should succeed
    storage.flush().expect("Flush should succeed");

    // Data should still be accessible
    assert!(storage.has_block(1, &root).unwrap());
}

/// Test block count accuracy.
#[test]
fn test_block_count() {
    let tmp = TempDir::new().unwrap();
    let storage = BlockStorage::open(tmp.path()).unwrap();

    assert_eq!(storage.block_count().unwrap(), 0);

    // Add blocks
    for i in 0..25u64 {
        let root = make_root(i / 5, i);
        let block = create_test_block(i / 5, i, &[i as u8]);
        storage.put_block(i / 5, &root, &block).unwrap();
    }

    assert_eq!(storage.block_count().unwrap(), 25);
}
