use super::*;
use crate::disk::StorageMetadata;

//= spec/ring/01-theory.md#core-requirements
//= type=test
//# `RING-CORE-001` Region starts and region sizes MUST be aligned to
//# the backing flash erase-block size so every region can be erased
//# independently.
#[test]
fn requirement_mock_flash_regions_occupy_fixed_independent_storage_spans() {
    let mut flash = MockFlash::<16, 3, 16>::new(0xff);
    flash.write_region(0, 0, &[0x11, 0x22]).unwrap();
    flash.write_region(1, 0, &[0x33, 0x44]).unwrap();

    let mut region_zero_prefix = [0u8; 2];
    let mut region_one_prefix = [0u8; 2];
    flash.read_storage(16, &mut region_zero_prefix).unwrap();
    flash.read_storage(32, &mut region_one_prefix).unwrap();
    assert_eq!(region_zero_prefix, [0x11, 0x22]);
    assert_eq!(region_one_prefix, [0x33, 0x44]);

    flash.erase_region(0).unwrap();
    flash.read_storage(16, &mut region_zero_prefix).unwrap();
    flash.read_storage(32, &mut region_one_prefix).unwrap();
    assert_eq!(region_zero_prefix, [0xff, 0xff]);
    assert_eq!(region_one_prefix, [0x33, 0x44]);
}

//= spec/ring/08-durability-formatting.md#format-storage-on-disk-initialization
//= type=test
//# `RING-FORMAT-STORAGE-PRE-001` Backing storage MUST be writable and erasable at region
//# granularity.
#[test]
fn requirement_mock_flash_supports_region_granularity_write_and_erase() {
    let mut flash = MockFlash::<8, 2, 16>::new(0xff);
    flash.write_region(0, 0, &[1, 2, 3, 4]).unwrap();

    let mut buffer = [0u8; 8];
    flash
        .read_region(0, 0, buffer.len(), |bytes| buffer.copy_from_slice(bytes))
        .unwrap();
    assert_eq!(buffer[..4], [1, 2, 3, 4]);

    flash.erase_region(0).unwrap();
    flash
        .read_region(0, 0, buffer.len(), |bytes| buffer.copy_from_slice(bytes))
        .unwrap();
    assert_eq!(buffer, [0xff; 8]);
}

//= spec/mock.md#mock-flash-requirements
//= type=test
//# `RING-IMPL-REGRESSION-037` Mock flash metadata read/write operations MUST persist metadata and
//# log write/read metadata operations in order.
#[test]
fn requirement_metadata_operations_are_logged() {
    let mut flash = MockFlash::<64, 4, 8>::new(0xff);
    let metadata = StorageMetadata::new(64, 4, 1, 4, 0xff, 0xa5).unwrap();

    flash.write_metadata(metadata).unwrap();
    let read_back = flash.read_metadata().unwrap();

    assert_eq!(read_back, Some(metadata));
    assert_eq!(
        flash.operations(),
        &[MockOperation::WriteMetadata, MockOperation::ReadMetadata]
    );
}

//= spec/mock.md#mock-flash-requirements
//= type=test
//# `RING-IMPL-REGRESSION-038` Mock flash storage reads MUST span metadata and data regions by
//# absolute offset and reject out-of-bounds reads.
#[test]
fn requirement_read_storage_spans_metadata_and_data_regions_with_bounds_checks() {
    let mut flash = MockFlash::<4, 2, 16>::new(0xff);
    flash.write_region(0, 0, &[0x10, 0x11, 0x12, 0x13]).unwrap();
    flash.write_region(1, 0, &[0x20, 0x21, 0x22, 0x23]).unwrap();

    let mut buffer = [0u8; 6];
    flash.read_storage(2, &mut buffer).unwrap();
    assert_eq!(buffer, [0xff, 0xff, 0x10, 0x11, 0x12, 0x13]);

    flash.read_storage(6, &mut buffer).unwrap();
    assert_eq!(buffer, [0x12, 0x13, 0x20, 0x21, 0x22, 0x23]);

    assert_eq!(
        flash.read_storage(12, &mut [0u8; 1]),
        Err(MockError::OutOfBounds)
    );
}

//= spec/mock.md#mock-flash-requirements
//= type=test
//# `RING-IMPL-REGRESSION-039` Mock flash metadata writes MUST fail without changing metadata when
//# the metadata region is smaller than encoded StorageMetadata.
#[test]
fn requirement_write_metadata_requires_metadata_region_large_enough() {
    let mut flash = MockFlash::<8, 4, 8>::new(0xff);
    let metadata = StorageMetadata::new(8, 4, 1, 4, 0xff, 0xa5).unwrap();

    assert_eq!(flash.write_metadata(metadata), Err(MockError::OutOfBounds));
    assert_eq!(flash.metadata(), None);
}

//= spec/mock.md#mock-flash-requirements
//= type=test
//# `RING-IMPL-REGRESSION-040` Mock flash metadata writes MUST succeed when the metadata region
//# exactly matches encoded StorageMetadata and persist decodable metadata.
#[test]
fn requirement_write_metadata_accepts_exact_metadata_region_size() {
    let mut flash = MockFlash::<{ StorageMetadata::ENCODED_LEN }, 4, 8>::new(0xff);
    let metadata =
        StorageMetadata::new(StorageMetadata::ENCODED_LEN as u32, 4, 1, 4, 0xff, 0xa5).unwrap();

    flash.write_metadata(metadata).unwrap();

    assert_eq!(flash.metadata(), Some(&metadata));
    assert_eq!(
        StorageMetadata::decode(&flash.metadata_region[..]).unwrap(),
        metadata
    );
}

//= spec/mock.md#mock-flash-requirements
//= type=test
//# `RING-IMPL-REGRESSION-041` FlashIo metadata operations on MockFlash MUST delegate to mock
//# metadata storage and return the persisted metadata.
#[test]
fn requirement_flash_io_trait_write_metadata_delegates_to_mock_flash() {
    let mut flash = MockFlash::<64, 4, 8>::new(0xff);
    let metadata = StorageMetadata::new(64, 4, 1, 4, 0xff, 0xa5).unwrap();

    crate::FlashIo::write_metadata(&mut flash, metadata).unwrap();

    assert_eq!(flash.metadata(), Some(&metadata));
    assert_eq!(
        crate::FlashIo::read_metadata(&mut flash).unwrap(),
        Some(metadata)
    );
}

//= spec/mock.md#mock-flash-requirements
//= type=test
//# `RING-IMPL-REGRESSION-042` Mock flash erase/write/read/sync operations MUST perform the
//# operation and log each operation with region, offset, and length details.
#[test]
fn requirement_erase_write_read_and_sync_are_logged() {
    let mut flash = MockFlash::<16, 2, 16>::new(0xff);

    flash.write_region(1, 4, b"bor").unwrap();
    let mut buffer = [0u8; 3];
    flash
        .read_region(1, 4, buffer.len(), |bytes| buffer.copy_from_slice(bytes))
        .unwrap();
    flash.erase_region(1).unwrap();
    flash.sync().unwrap();

    assert_eq!(&buffer, b"bor");
    assert_eq!(
        flash.operations(),
        &[
            MockOperation::WriteRegion {
                region_index: 1,
                offset: 4,
                len: 3,
            },
            MockOperation::ReadRegion {
                region_index: 1,
                offset: 4,
                len: 3,
            },
            MockOperation::EraseRegion { region_index: 1 },
            MockOperation::Sync,
        ]
    );
}

//= spec/mock.md#mock-flash-requirements
//= type=test
//# `RING-IMPL-REGRESSION-043` Erasing a mock flash region MUST restore every byte in that region to
//# the erased byte.
#[test]
fn requirement_erase_restores_erased_bytes() {
    let mut flash = MockFlash::<8, 1, 8>::new(0xff);
    flash.write_region(0, 0, &[1, 2, 3, 4]).unwrap();
    flash.erase_region(0).unwrap();

    let mut buffer = [0u8; 8];
    flash
        .read_region(0, 0, buffer.len(), |bytes| buffer.copy_from_slice(bytes))
        .unwrap();
    assert_eq!(buffer, [0xff; 8]);
}

//= spec/ring/08-durability-formatting.md#format-storage-on-disk-initialization
//= type=test
//# RING-FORMAT-STORAGE-PRE-006 `region_count >= 2 + min_free_regions`.
#[test]
fn requirement_format_empty_store_rejects_too_few_regions() {
    let mut flash = MockFlash::<64, 2, 16>::new(0xff);
    let error = flash.format_empty_store(1, 8, 0xa5).unwrap_err();
    assert_eq!(
        error,
        MockFormatError::InsufficientRegions {
            region_count: 2,
            min_free_regions: 1,
        }
    );

    let mut tiny = MockFlash::<7, 3, 16>::new(0xff);
    assert_eq!(
        tiny.format_empty_store(1, 8, 0xa5),
        Err(MockFormatError::RegionSizeTooSmall {
            region_size: 7,
            min_region_size: 32,
        })
    );
}

//= spec/mock.md#mock-flash-requirements
//= type=test
//# `RING-IMPL-REGRESSION-044` Formatting an empty mock store MUST accept the exact minimum region
//# count and persist matching metadata.
#[test]
fn requirement_format_empty_store_accepts_exact_minimum_region_count() {
    let mut flash = MockFlash::<64, 3, 16>::new(0xff);
    let metadata = flash.format_empty_store(1, 8, 0xa5).unwrap();

    assert_eq!(metadata.region_count, 3);
    assert_eq!(flash.metadata(), Some(&metadata));
}

//= spec/ring/08-durability-formatting.md#format-storage-on-disk-initialization
//= type=test
//# `RING-FORMAT-STORAGE-PRE-003` Region `0` MUST be reserved as the initial WAL region.
#[test]
fn requirement_format_empty_store_reserves_region_zero_as_initial_wal_region() {
    let mut flash = MockFlash::<64, 4, 32>::new(0xff);
    flash.format_empty_store(1, 8, 0xa5).unwrap();

    let header = Header::decode(&flash.region_bytes(0).unwrap()[..Header::ENCODED_LEN]).unwrap();
    assert_eq!(header.collection_id, CollectionId(0));
    assert_eq!(header.collection_format, WAL_V1_FORMAT);
}

//= spec/ring/08-durability-formatting.md#format-storage-on-disk-initialization
//= type=test
//# `RING-FORMAT-STORAGE-003` Initialize region `0` as WAL:
//# write valid `Header` with `collection_id = 0`,
//# `collection_format = wal_v1`, and `sequence = 0`,
//# write a valid `WalRegionPrologue` with `wal_head_region_index = 0`,
//# then sync region `0`.
#[test]
fn requirement_format_empty_store_initializes_region_zero_with_wal_header_and_prologue() {
    let mut flash = MockFlash::<64, 4, 32>::new(0xff);
    let metadata = flash.format_empty_store(1, 8, 0xa5).unwrap();

    let header = Header::decode(&flash.region_bytes(0).unwrap()[..Header::ENCODED_LEN]).unwrap();
    assert_eq!(header.sequence, 0);
    assert_eq!(header.collection_id, CollectionId(0));
    assert_eq!(header.collection_format, WAL_V1_FORMAT);

    let prologue_offset = Header::ENCODED_LEN;
    let prologue_end = prologue_offset + WalRegionPrologue::ENCODED_LEN;
    let prologue = WalRegionPrologue::decode(
        &flash.region_bytes(0).unwrap()[prologue_offset..prologue_end],
        metadata.region_count,
    )
    .unwrap();
    assert_eq!(prologue.wal_head_region_index, 0);

    assert!(flash.operations().contains(&MockOperation::WriteRegion {
        region_index: 0,
        offset: 0,
        len: Header::ENCODED_LEN,
    }));
    assert!(flash.operations().contains(&MockOperation::WriteRegion {
        region_index: 0,
        offset: Header::ENCODED_LEN,
        len: WalRegionPrologue::ENCODED_LEN,
    }));
    assert_eq!(flash.operations().last(), Some(&MockOperation::Sync));
}

//= spec/ring/08-durability-formatting.md#format-storage-on-disk-initialization
//= type=test
//# `RING-FORMAT-STORAGE-POST-003` The free list MUST contain every non-WAL region in ascending
//# region-index order.
#[test]
fn requirement_format_empty_store_populates_free_list_in_ascending_order() {
    let mut flash = MockFlash::<64, 4, 32>::new(0xff);
    let metadata = flash.format_empty_store(1, 8, 0xa5).unwrap();

    assert_eq!(flash.metadata(), Some(&metadata));

    let footer_offset = 64 - FreePointerFooter::ENCODED_LEN;
    assert!(flash.region_bytes(1).unwrap()[..footer_offset]
        .iter()
        .all(|byte| *byte == 0xff));
    assert!(flash.region_bytes(2).unwrap()[..footer_offset]
        .iter()
        .all(|byte| *byte == 0xff));
    assert!(flash.region_bytes(3).unwrap()[..footer_offset]
        .iter()
        .all(|byte| *byte == 0xff));
    let region1 =
        FreePointerFooter::decode(&flash.region_bytes(1).unwrap()[footer_offset..], 0xff).unwrap();
    let region2 =
        FreePointerFooter::decode(&flash.region_bytes(2).unwrap()[footer_offset..], 0xff).unwrap();
    let region3 =
        FreePointerFooter::decode(&flash.region_bytes(3).unwrap()[footer_offset..], 0xff).unwrap();

    assert_eq!(region1.next_tail, Some(2));
    assert_eq!(region2.next_tail, Some(3));
    assert_eq!(region3.next_tail, None);
}

//= spec/ring/05-disk-format.md#free-pointer-footer
//= type=test
//# `RING-FREE-001` The free-pointer footer MUST occupy the final eight
//# bytes of the region.
#[test]
fn requirement_format_empty_store_writes_free_pointer_footer_in_final_eight_bytes() {
    let mut flash = MockFlash::<64, 4, 32>::new(0xff);
    flash.format_empty_store(1, 8, 0xa5).unwrap();

    let footer_offset = 64 - FreePointerFooter::ENCODED_LEN;
    assert_eq!(footer_offset + FreePointerFooter::ENCODED_LEN, 64);
    assert!(flash.region_bytes(1).unwrap()[..footer_offset]
        .iter()
        .all(|byte| *byte == 0xff));

    let footer =
        FreePointerFooter::decode(&flash.region_bytes(1).unwrap()[footer_offset..], 0xff).unwrap();
    assert_eq!(footer.next_tail, Some(2));
}

//= spec/mock.md#mock-flash-requirements
//= type=test
//# `RING-IMPL-REGRESSION-045` Formatting an empty mock store MUST leave reserved bytes after
//# encoded StorageMetadata erased.
#[test]
fn requirement_format_empty_store_leaves_reserved_metadata_bytes_erased() {
    let mut flash = MockFlash::<64, 4, 32>::new(0xff);
    flash.format_empty_store(1, 8, 0xa5).unwrap();

    let mut metadata_region = [0u8; 64];
    flash.read_storage(0, &mut metadata_region).unwrap();
    assert!(metadata_region[StorageMetadata::ENCODED_LEN..]
        .iter()
        .all(|byte| *byte == 0xff));
}

//= spec/ring/05-disk-format.md#storage-requirements
//= type=test
//# `RING-STORAGE-010` The metadata region MUST occupy exactly one `region_size` span at storage
//# offset `0`, MUST NOT be counted in `region_count`, and data region `0` MUST begin immediately
//# after that metadata region.
#[test]
fn requirement_format_empty_store_places_region_zero_immediately_after_metadata_region() {
    let mut flash = MockFlash::<64, 4, 32>::new(0xff);
    let metadata = flash.format_empty_store(1, 8, 0xa5).unwrap();

    assert_eq!(metadata.region_count, 4);

    let mut metadata_region = [0u8; 64];
    flash.read_storage(0, &mut metadata_region).unwrap();
    assert_eq!(StorageMetadata::decode(&metadata_region).unwrap(), metadata);

    let mut header_bytes = [0u8; Header::ENCODED_LEN];
    flash.read_storage(64, &mut header_bytes).unwrap();
    assert_eq!(
        header_bytes,
        flash.region_bytes(0).unwrap()[..Header::ENCODED_LEN]
    );

    let header = Header::decode(&header_bytes).unwrap();
    assert_eq!(header.collection_id, CollectionId(0));
    assert_eq!(header.collection_format, WAL_V1_FORMAT);
}

//= spec/ring/05-disk-format.md#storage-requirements
//= type=test
//# `RING-STORAGE-001` Storage MUST begin with a static metadata region that records version and
//# configuration parameters that do not change after initialization.
#[test]
fn requirement_format_empty_store_begins_with_static_metadata_region() {
    let mut flash = MockFlash::<64, 4, 32>::new(0xff);
    let metadata = flash.format_empty_store(1, 8, 0xa5).unwrap();

    let mut metadata_region = [0u8; 64];
    flash.read_storage(0, &mut metadata_region).unwrap();
    assert_eq!(StorageMetadata::decode(&metadata_region).unwrap(), metadata);

    let mut region_zero_prefix = [0u8; Header::ENCODED_LEN];
    flash.read_storage(64, &mut region_zero_prefix).unwrap();
    let wal_header = Header::decode(&region_zero_prefix).unwrap();
    assert_eq!(wal_header.collection_id, CollectionId(0));
}
