use super::test_utils::*;
use crate::{
    account_info::{Account, AccountInfo},
    program_error::ProgramError,
    sysvars::{clock::Slot, slot_hashes::*},
};
use core::{
    mem::{align_of, size_of},
    ptr,
};

extern crate std;
use std::io::Write;
use std::vec::Vec;

#[test]
fn test_layout_constants() {
    assert_eq!(NUM_ENTRIES_SIZE, size_of::<u64>());
    assert_eq!(SLOT_SIZE, size_of::<u64>());
    assert_eq!(HASH_BYTES, 32);
    assert_eq!(ENTRY_SIZE, size_of::<u64>() + 32);
    assert_eq!(MAX_SIZE, 20_488);
    assert_eq!(size_of::<SlotHashEntry>(), ENTRY_SIZE);
    assert_eq!(align_of::<SlotHashEntry>(), align_of::<[u8; 8]>());
    assert_eq!(
        SLOTHASHES_ID,
        [
            6, 167, 213, 23, 25, 47, 10, 175, 198, 242, 101, 227, 251, 119, 204, 122, 218, 130,
            197, 41, 208, 190, 59, 19, 110, 45, 0, 85, 32, 0, 0, 0,
        ]
    );

    pub fn check_base58(input_bytes: &[u8], expected_b58: &str) {
        assert_eq!(five8_const::decode_32_const(expected_b58), input_bytes);
    }

    check_base58(
        &SLOTHASHES_ID,
        "SysvarS1otHashes111111111111111111111111111",
    );
}

#[test]
fn test_binary_search_no_std() {
    const TEST_NUM_ENTRIES: usize = 512;
    const START_SLOT: u64 = 2000;

    let entries =
        generate_mock_entries(TEST_NUM_ENTRIES, START_SLOT, DecrementStrategy::Average1_05);
    let data = create_mock_data(&entries);
    let entry_count = entries.len();

    let first_slot = entries[0].0;
    let mid_index = entry_count / 2;
    let mid_slot = entries[mid_index].0;
    let last_slot = entries[entry_count - 1].0;

    let slot_hashes = unsafe { SlotHashes::new_unchecked(data.as_slice()) };

    assert_eq!(slot_hashes.position(first_slot), Some(0));

    let expected_mid_index = Some(mid_index);
    let actual_pos_mid = slot_hashes.position(mid_slot);

    // Extract surrounding entries for context in case of failure
    let start_idx = mid_index.saturating_sub(2);
    let end_idx = core::cmp::min(entry_count, mid_index.saturating_add(3));
    let surrounding_slots: Vec<_> = entries[start_idx..end_idx].iter().map(|e| e.0).collect();
    assert_eq!(
        actual_pos_mid, expected_mid_index,
        "position({}) failed! Surrounding slots: {:?}",
        mid_slot, surrounding_slots
    );

    assert_eq!(slot_hashes.position(last_slot), Some(entry_count - 1));

    assert_eq!(slot_hashes.position(START_SLOT + 1), None);

    // Find an actual gap to test a guaranteed non-existent internal slot
    let mut missing_internal_slot = None;
    for i in 0..(entries.len() - 1) {
        if entries[i].0 > entries[i + 1].0 + 1 {
            missing_internal_slot = Some(entries[i + 1].0 + 1);
            break;
        }
    }
    assert!(
        missing_internal_slot.is_some(),
        "Test requires at least one gap between slots"
    );
    assert_eq!(slot_hashes.position(missing_internal_slot.unwrap()), None);

    assert_eq!(slot_hashes.get_hash(first_slot), Some(&entries[0].1));
    assert_eq!(slot_hashes.get_hash(mid_slot), Some(&entries[mid_index].1));
    assert_eq!(
        slot_hashes.get_hash(last_slot),
        Some(&entries[entry_count - 1].1)
    );
    assert_eq!(slot_hashes.get_hash(START_SLOT + 1), None);

    // Test empty list explicitly
    let empty_entries = generate_mock_entries(0, START_SLOT, DecrementStrategy::Strictly1);
    let empty_data = create_mock_data(&empty_entries);
    let empty_hashes = unsafe { SlotHashes::new_unchecked(empty_data.as_slice()) };
    assert_eq!(empty_hashes.get_hash(100), None);

    let pos_start_plus_1 = slot_hashes.position(START_SLOT + 1);
    assert!(
        pos_start_plus_1.is_none(),
        "position(START_SLOT + 1) should be None"
    );
}

#[test]
fn test_basic_getters_and_iterator_no_std() {
    const NUM_ENTRIES: usize = 512;
    const START_SLOT: u64 = 2000;
    let entries = generate_mock_entries(NUM_ENTRIES, START_SLOT, DecrementStrategy::Strictly1);
    let data = create_mock_data(&entries);
    let slot_hashes = unsafe { SlotHashes::new_unchecked(data.as_slice()) };

    assert_eq!(slot_hashes.len(), NUM_ENTRIES);

    let entry0 = slot_hashes.get_entry(0);
    assert!(entry0.is_some());
    assert_eq!(entry0.unwrap().slot(), START_SLOT); // Check against start slot
    assert_eq!(entry0.unwrap().hash, [0u8; HASH_BYTES]); // First generated hash is [0u8; 32]

    let entry2 = slot_hashes.get_entry(NUM_ENTRIES - 1); // Last entry
    assert!(entry2.is_some());
    assert_eq!(entry2.unwrap().slot(), entries[NUM_ENTRIES - 1].0);
    assert_eq!(entry2.unwrap().hash, entries[NUM_ENTRIES - 1].1);
    assert!(slot_hashes.get_entry(NUM_ENTRIES).is_none()); // Out of bounds

    for (i, entry) in slot_hashes.into_iter().enumerate() {
        assert_eq!(entry.slot(), entries[i].0);
        assert_eq!(entry.hash, entries[i].1);
    }
    assert!(slot_hashes.into_iter().nth(NUM_ENTRIES).is_none());

    // Test ExactSizeIterator hint
    let mut iter_hint = slot_hashes.into_iter();
    assert_eq!(iter_hint.len(), NUM_ENTRIES);
    iter_hint.next();
    assert_eq!(iter_hint.len(), NUM_ENTRIES - 1);
    // Skip to end
    for _ in 1..NUM_ENTRIES {
        iter_hint.next();
    }
    iter_hint.next();
    assert_eq!(iter_hint.len(), 0);

    // Test empty case
    let empty_data = create_mock_data(&[]);
    let empty_hashes = unsafe { SlotHashes::new_unchecked(empty_data.as_slice()) };
    assert_eq!(empty_hashes.len(), 0);
    assert!(empty_hashes.get_entry(0).is_none());
    assert!(empty_hashes.into_iter().next().is_none());
}

#[test]
fn test_entry_count_no_std() {
    // Valid data (2 entries)
    let entries: &[(Slot, Hash)] = &[(100, [1u8; HASH_BYTES]), (98, [2u8; HASH_BYTES])];
    let data = create_mock_data(entries);
    let slot_hashes = unsafe { SlotHashes::new_unchecked(data.as_slice()) };
    assert_eq!(slot_hashes.len(), 2);

    // Too small buffer should fail new()
    let num_entries = entries.len() as u64;
    let data_len = NUM_ENTRIES_SIZE + entries.len() * ENTRY_SIZE;
    let mut small_data = std::vec![0u8; data_len];
    small_data[0..NUM_ENTRIES_SIZE].copy_from_slice(&num_entries.to_le_bytes());
    let mut offset = NUM_ENTRIES_SIZE;
    for (slot, hash) in entries {
        small_data[offset..offset + SLOT_SIZE].copy_from_slice(&slot.to_le_bytes());
        small_data[offset + SLOT_SIZE..offset + ENTRY_SIZE].copy_from_slice(hash);
        offset += ENTRY_SIZE;
    }
    let res1 = SlotHashes::new(small_data.as_slice());
    assert!(
        res1.is_ok(),
        "SlotHashes::new should succeed with a correctly sized buffer"
    );
    let slot_hashes_from_small = res1.unwrap();
    assert_eq!(slot_hashes_from_small.len(), entries.len());

    // Empty data is valid
    let empty_data = create_mock_data(&[]);
    let empty_hashes = unsafe { SlotHashes::new_unchecked(empty_data.as_slice()) };
    assert_eq!(empty_hashes.len(), 0);
}

#[test]
fn test_get_entry_unchecked_no_std() {
    let single_entry: &[(Slot, Hash)] = &[(100, [1u8; HASH_BYTES])];
    let data = create_mock_data(single_entry);
    let slot_hashes = unsafe { SlotHashes::new_unchecked(data.as_slice()) };

    let entry = unsafe { slot_hashes.get_entry_unchecked(0) };
    assert_eq!(entry.slot(), 100);
    assert_eq!(entry.hash, [1u8; HASH_BYTES]);
}

#[test]
fn test_get_entry_unchecked_last_no_std() {
    const COUNT: usize = 8;
    const START_SLOT: u64 = 600;
    let entries = generate_mock_entries(COUNT, START_SLOT, DecrementStrategy::Strictly1);
    let data = create_mock_data(&entries);
    let sh = unsafe { SlotHashes::new_unchecked(data.as_slice()) };

    let last = unsafe { sh.get_entry_unchecked(COUNT - 1) };
    assert_eq!(last.slot(), entries[COUNT - 1].0);
    assert_eq!(last.hash, entries[COUNT - 1].1);
}

#[test]
fn test_iterator_into_ref_no_std() {
    const NUM: usize = 16;
    const START: u64 = 100;
    let entries = generate_mock_entries(NUM, START, DecrementStrategy::Strictly1);
    let data = create_mock_data(&entries);
    let sh = unsafe { SlotHashes::new_unchecked(data.as_slice()) };

    // Collect slots via iterator
    let mut sum: u64 = 0;
    for e in &sh {
        sum += e.slot();
    }
    let expected_sum: u64 = entries.iter().map(|(s, _)| *s).sum();
    assert_eq!(sum, expected_sum);

    let iter = (&sh).into_iter();
    assert_eq!(iter.len(), sh.len());
}

// Tests to verify mock data helpers
#[test]
fn test_mock_data_max_entries_boundary() {
    let entries = generate_mock_entries(MAX_ENTRIES, 1000, DecrementStrategy::Strictly1);
    let data = create_mock_data(&entries);
    let sh = unsafe { SlotHashes::new_unchecked(data.as_slice()) };
    assert_eq!(sh.len(), MAX_ENTRIES);
}

#[test]
fn test_mock_data_raw_byte_layout() {
    let entries = &[(100u64, [0xAB; 32])];
    let data = create_mock_data(entries);
    // length prefix
    assert_eq!(&data[0..8], &1u64.to_le_bytes());
    // slot bytes
    assert_eq!(&data[8..16], &100u64.to_le_bytes());
    // hash bytes
    assert_eq!(&data[16..48], &[0xAB; 32]);
}

#[test]
fn test_read_entry_count_from_bytes() {
    let entry_count = 42u64;
    let mut data = [0u8; 16];
    data[0..8].copy_from_slice(&entry_count.to_le_bytes());

    let result = read_entry_count_from_bytes(&data);
    assert_eq!(result, Some(42));

    let zero_count = 0u64;
    let mut zero_data = [0u8; 8];
    zero_data.copy_from_slice(&zero_count.to_le_bytes());

    let zero_result = read_entry_count_from_bytes(&zero_data);
    assert_eq!(zero_result, Some(0));

    let max_count = MAX_ENTRIES as u64;
    let mut max_data = [0u8; 8];
    max_data.copy_from_slice(&max_count.to_le_bytes());

    let max_result = read_entry_count_from_bytes(&max_data);
    assert_eq!(max_result, Some(MAX_ENTRIES));
}

fn mock_fetch_into_unchecked(
    mock_sysvar_data: &[u8],
    buffer: &mut [u8],
    offset: u64,
) -> Result<(), ProgramError> {
    let offset = offset as usize;
    if offset >= mock_sysvar_data.len() {
        return Err(ProgramError::InvalidArgument);
    }

    let available_len = mock_sysvar_data.len() - offset;
    let copy_len = core::cmp::min(buffer.len(), available_len);

    buffer[..copy_len].copy_from_slice(&mock_sysvar_data[offset..offset + copy_len]);
    Ok(())
}

/// Verifies that the mock byte-copy helper (`mock_fetch_into_unchecked`) obeys
/// the same offset semantics we expect from the real `raw::fetch_into_*` API.
///
/// This is purely an internal byte-math test; it does not call the
/// production syscall wrapper and therefore does not attest that the runtime
/// offset logic works.  Its value is guarding against mistakes
/// in the offset arithmetic used by other in-test helpers.
#[test]
fn test_mock_offset_copy() {
    // Create mock sysvar data: 8-byte length + 3 entries
    let entries = &[
        (100u64, [1u8; HASH_BYTES]),
        (99u64, [2u8; HASH_BYTES]),
        (98u64, [3u8; HASH_BYTES]),
    ];
    let mock_sysvar_data = create_mock_data(entries);

    // Test offset 0 (full data)
    let mut buffer_full = std::vec![0u8; mock_sysvar_data.len()];
    mock_fetch_into_unchecked(&mock_sysvar_data, &mut buffer_full, 0).unwrap();
    assert_eq!(buffer_full, mock_sysvar_data);

    // Test offset 8 (skip length prefix, get entries only)
    let entries_size = 3 * ENTRY_SIZE;
    let mut buffer_entries = std::vec![0u8; entries_size];
    mock_fetch_into_unchecked(&mock_sysvar_data, &mut buffer_entries, 8).unwrap();
    assert_eq!(buffer_entries, &mock_sysvar_data[8..8 + entries_size]);

    // Test offset 8 + ENTRY_SIZE (skip first entry)
    let remaining_entries_size = 2 * ENTRY_SIZE;
    let mut buffer_skip_first = std::vec![0u8; remaining_entries_size];
    let skip_first_offset = 8 + ENTRY_SIZE;
    mock_fetch_into_unchecked(
        &mock_sysvar_data,
        &mut buffer_skip_first,
        skip_first_offset as u64,
    )
    .unwrap();
    assert_eq!(
        buffer_skip_first,
        &mock_sysvar_data[skip_first_offset..skip_first_offset + remaining_entries_size]
    );

    // Test partial read with small buffer
    let mut small_buffer = [0u8; 16]; // Only 16 bytes
    mock_fetch_into_unchecked(&mock_sysvar_data, &mut small_buffer, 0).unwrap();
    assert_eq!(small_buffer, &mock_sysvar_data[0..16]);

    // Test offset beyond data (should fail)
    let mut buffer_beyond = [0u8; 10];
    let beyond_offset = mock_sysvar_data.len() as u64;
    assert!(
        mock_fetch_into_unchecked(&mock_sysvar_data, &mut buffer_beyond, beyond_offset).is_err()
    );
}

#[test]
fn test_entries_exposed_no_std() {
    let entries = generate_mock_entries(8, 80, DecrementStrategy::Strictly1);
    let data = create_mock_data(&entries);
    let sh = unsafe { SlotHashes::new_unchecked(data.as_slice()) };

    let slice = sh.entries();
    assert_eq!(slice.len(), entries.len());
    for (i, e) in slice.iter().enumerate() {
        assert_eq!(e.slot(), entries[i].0);
        assert_eq!(e.hash, entries[i].1);
    }
}

#[test]
fn test_safe_vs_unsafe_getters_consistency() {
    let entries = generate_mock_entries(16, 200, DecrementStrategy::Strictly1);
    let data = create_mock_data(&entries);
    let sh = unsafe { SlotHashes::new_unchecked(data.as_slice()) };

    for i in 0..entries.len() {
        let safe_entry = sh.get_entry(i).unwrap();
        let unsafe_entry = unsafe { sh.get_entry_unchecked(i) };
        assert_eq!(safe_entry, unsafe_entry);
    }

    assert_eq!(sh.len(), entries.len());
}

#[test]
fn test_entry_count_header_too_short() {
    let short = [0u8; 4];
    assert!(SlotHashes::new(&short[..]).is_err());
    assert_eq!(read_entry_count_from_bytes(&short), None);
}

#[test]
fn test_log_function() {
    let test_hash: Hash = [
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
        26, 27, 28, 29, 30, 31, 32,
    ];

    // Should not panic
    log(&test_hash);
}

#[test]
fn test_from_account_info_constructor() {
    std::io::stderr().flush().unwrap();

    const NUM_ENTRIES: usize = 3;
    const START_SLOT: u64 = 1234;

    let mock_entries = generate_mock_entries(NUM_ENTRIES, START_SLOT, DecrementStrategy::Strictly1);
    let data = create_mock_data(&mock_entries);

    let mut aligned_backing: Vec<u64>;
    let acct_ptr;

    unsafe {
        let header_size = core::mem::size_of::<AccountLayout>();
        let total_size = header_size + data.len();
        let word_len = (total_size + 7) / 8;
        aligned_backing = std::vec![0u64; word_len];
        let base_ptr = aligned_backing.as_mut_ptr() as *mut u8;

        let header_ptr = base_ptr as *mut AccountLayout;
        ptr::write(
            header_ptr,
            AccountLayout {
                borrow_state: crate::entrypoint::NON_DUP_MARKER,
                is_signer: 0,
                is_writable: 0,
                executable: 0,
                resize_delta: 0,
                key: SLOTHASHES_ID,
                owner: [0u8; 32],
                lamports: 0,
                data_len: data.len() as u64,
            },
        );

        ptr::copy_nonoverlapping(data.as_ptr(), base_ptr.add(header_size), data.len());

        acct_ptr = base_ptr as *mut Account;
    }

    let account_info = AccountInfo { raw: acct_ptr };

    let slot_hashes = SlotHashes::from_account_info(&account_info)
        .expect("from_account_info should succeed with well-formed data");

    assert_eq!(slot_hashes.len(), NUM_ENTRIES);
    for (i, entry) in slot_hashes.into_iter().enumerate() {
        assert_eq!(entry.slot(), mock_entries[i].0);
        assert_eq!(entry.hash, mock_entries[i].1);
    }
}

/// Host-side sanity test: ensure the `SlotHashes::fetch()` helper compiles and
/// allocates a MAX_SIZE-sized buffer without panicking.
///
/// On non-Solana targets the underlying syscall is stubbed; the returned buffer
/// is zero-initialized and contains zero entries.  We overwrite
/// that buffer with deterministic fixture data and then exercise the normal
/// `SlotHashes` getters to make sure the view itself works.  We do not verify
/// that the syscall populated real on-chain bytes, as doing so requires an
/// environment outside the scope of host `cargo test`.
#[cfg(feature = "std")]
#[test]
fn test_fetch_allocates_buffer_host() {
    const START_SLOT: u64 = 500;
    let entries = generate_mock_entries(5, START_SLOT, DecrementStrategy::Strictly1);
    let data = create_mock_data(&entries);

    // This should allocate a 20_488-byte boxed slice and *not* panic.
    let mut slot_hashes =
        SlotHashes::<std::boxed::Box<[u8]>>::fetch().expect("fetch() should allocate");

    // Overwrite the stubbed contents with known data so we can reuse the
    // remainder of the test harness.
    slot_hashes.data[..data.len()].copy_from_slice(&data);

    assert_eq!(slot_hashes.len(), entries.len());
    for (i, entry) in slot_hashes.into_iter().enumerate() {
        assert_eq!(entry.slot(), entries[i].0);
        assert_eq!(entry.hash, entries[i].1);
    }
}
