use crate::format::FormatError;
use crate::wire::ExtensionTlv;

pub const RAW_STREAM_CONTENT_MODEL_EXTENSION_TAG: u16 = 0x0007;
#[cfg(test)]
pub const RAW_STREAM_CONTENT_MODEL_EXTENSION_CRITICAL_TAG: u16 =
    0x8000 | RAW_STREAM_CONTENT_MODEL_EXTENSION_TAG;
pub const RAW_STREAM_CONTENT_MODEL_VALUE: &[u8] = b"raw_stream_v1";
pub const RAW_STREAM_UNSUPPORTED_MESSAGE: &str =
    "raw-stream content profile is not supported by the base v41 tar reader";

#[cfg(test)]
pub const RAW_STREAM_INDEX_ROOT_V1_MAGIC: [u8; 8] = *b"TZRSIDX1";
#[cfg(test)]
pub const RAW_STREAM_INDEX_ROOT_V1_VERSION: u16 = 1;
#[cfg(test)]
pub const RAW_STREAM_INDEX_ROOT_V1_LEN: usize = 112;
#[cfg(test)]
pub const RAW_FILE_ENTRY_V1_LEN: usize = 88;
#[cfg(test)]
pub const RAW_FRAME_ENTRY_V1_LEN: usize = 48;
#[cfg(test)]
pub const RAW_ENVELOPE_ENTRY_V1_LEN: usize = 56;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContentProfile {
    TarMemberV41,
    RawStreamV1,
}

#[cfg(test)]
pub fn serialize_raw_stream_content_model_extension() -> Vec<u8> {
    let mut bytes = Vec::with_capacity(6 + RAW_STREAM_CONTENT_MODEL_VALUE.len());
    bytes.extend_from_slice(&RAW_STREAM_CONTENT_MODEL_EXTENSION_CRITICAL_TAG.to_le_bytes());
    bytes.extend_from_slice(&(RAW_STREAM_CONTENT_MODEL_VALUE.len() as u32).to_le_bytes());
    bytes.extend_from_slice(RAW_STREAM_CONTENT_MODEL_VALUE);
    bytes
}

pub fn validate_raw_stream_content_model_extension(
    is_critical: bool,
    value: &[u8],
) -> Result<(), FormatError> {
    if !is_critical || value != RAW_STREAM_CONTENT_MODEL_VALUE {
        return Err(FormatError::MalformedKnownExtension(
            RAW_STREAM_CONTENT_MODEL_EXTENSION_TAG,
        ));
    }
    Ok(())
}

pub fn content_profile_from_extensions(
    extensions: &[ExtensionTlv<'_>],
) -> Result<ContentProfile, FormatError> {
    let mut profile = ContentProfile::TarMemberV41;
    for extension in extensions {
        let ext_tag = extension.tag & 0x7fff;
        let is_critical = extension.tag & 0x8000 != 0;
        if ext_tag == RAW_STREAM_CONTENT_MODEL_EXTENSION_TAG && is_critical {
            validate_raw_stream_content_model_extension(is_critical, extension.value)?;
            profile = ContentProfile::RawStreamV1;
        }
    }
    Ok(profile)
}

pub fn reject_unsupported_raw_stream_profile(
    extensions: &[ExtensionTlv<'_>],
) -> Result<(), FormatError> {
    match content_profile_from_extensions(extensions)? {
        ContentProfile::TarMemberV41 => Ok(()),
        ContentProfile::RawStreamV1 => Err(FormatError::ReaderUnsupported(
            RAW_STREAM_UNSUPPORTED_MESSAGE,
        )),
    }
}

#[cfg(test)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RawStreamIndexRootV1 {
    pub file_count: u32,
    pub frame_count: u64,
    pub envelope_count: u64,
    pub payload_block_count: u64,
    pub raw_total_size: u64,
    pub raw_content_sha256: [u8; 32],
    pub raw_file_entry_offset: u32,
    pub raw_frame_table_offset: u32,
    pub raw_envelope_table_offset: u32,
    pub string_pool_offset: u32,
    pub string_pool_size: u32,
}

#[cfg(test)]
impl RawStreamIndexRootV1 {
    pub fn parse(bytes: &[u8]) -> Result<Self, FormatError> {
        expect_len(
            "RawStreamIndexRootV1",
            RAW_STREAM_INDEX_ROOT_V1_LEN,
            bytes.len(),
        )?;
        expect_magic(
            "RawStreamIndexRootV1",
            RAW_STREAM_INDEX_ROOT_V1_MAGIC,
            &bytes[0..8],
        )?;
        let version = read_u16(bytes, 8, "RawStreamIndexRootV1")?;
        if version != RAW_STREAM_INDEX_ROOT_V1_VERSION {
            return Err(FormatError::InvalidMetadata {
                structure: "RawStreamIndexRootV1",
                reason: "unsupported version",
            });
        }
        expect_zero("RawStreamIndexRootV1", &bytes[10..16])?;
        expect_zero("RawStreamIndexRootV1", &bytes[20..24])?;
        expect_zero("RawStreamIndexRootV1", &bytes[108..112])?;
        Ok(Self {
            file_count: read_u32(bytes, 16, "RawStreamIndexRootV1")?,
            frame_count: read_u64(bytes, 24, "RawStreamIndexRootV1")?,
            envelope_count: read_u64(bytes, 32, "RawStreamIndexRootV1")?,
            payload_block_count: read_u64(bytes, 40, "RawStreamIndexRootV1")?,
            raw_total_size: read_u64(bytes, 48, "RawStreamIndexRootV1")?,
            raw_content_sha256: read_array_32(bytes, 56, "RawStreamIndexRootV1")?,
            raw_file_entry_offset: read_u32(bytes, 88, "RawStreamIndexRootV1")?,
            raw_frame_table_offset: read_u32(bytes, 92, "RawStreamIndexRootV1")?,
            raw_envelope_table_offset: read_u32(bytes, 96, "RawStreamIndexRootV1")?,
            string_pool_offset: read_u32(bytes, 100, "RawStreamIndexRootV1")?,
            string_pool_size: read_u32(bytes, 104, "RawStreamIndexRootV1")?,
        })
    }

    pub fn to_bytes(&self) -> [u8; RAW_STREAM_INDEX_ROOT_V1_LEN] {
        let mut bytes = [0u8; RAW_STREAM_INDEX_ROOT_V1_LEN];
        bytes[0..8].copy_from_slice(&RAW_STREAM_INDEX_ROOT_V1_MAGIC);
        write_u16(&mut bytes, 8, RAW_STREAM_INDEX_ROOT_V1_VERSION);
        write_u32(&mut bytes, 16, self.file_count);
        write_u64(&mut bytes, 24, self.frame_count);
        write_u64(&mut bytes, 32, self.envelope_count);
        write_u64(&mut bytes, 40, self.payload_block_count);
        write_u64(&mut bytes, 48, self.raw_total_size);
        bytes[56..88].copy_from_slice(&self.raw_content_sha256);
        write_u32(&mut bytes, 88, self.raw_file_entry_offset);
        write_u32(&mut bytes, 92, self.raw_frame_table_offset);
        write_u32(&mut bytes, 96, self.raw_envelope_table_offset);
        write_u32(&mut bytes, 100, self.string_pool_offset);
        write_u32(&mut bytes, 104, self.string_pool_size);
        bytes
    }
}

#[cfg(test)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RawFileEntryV1 {
    pub path_hash: [u8; 8],
    pub path_offset: u32,
    pub path_length: u32,
    pub file_data_size: u64,
    pub first_frame_index: u64,
    pub frame_count: u64,
    pub mode: u32,
    pub mtime: u64,
    pub content_sha256: [u8; 32],
}

#[cfg(test)]
impl RawFileEntryV1 {
    pub fn parse(bytes: &[u8]) -> Result<Self, FormatError> {
        expect_len("RawFileEntryV1", RAW_FILE_ENTRY_V1_LEN, bytes.len())?;
        expect_zero("RawFileEntryV1", &bytes[44..48])?;
        Ok(Self {
            path_hash: read_array_8(bytes, 0, "RawFileEntryV1")?,
            path_offset: read_u32(bytes, 8, "RawFileEntryV1")?,
            path_length: read_u32(bytes, 12, "RawFileEntryV1")?,
            file_data_size: read_u64(bytes, 16, "RawFileEntryV1")?,
            first_frame_index: read_u64(bytes, 24, "RawFileEntryV1")?,
            frame_count: read_u64(bytes, 32, "RawFileEntryV1")?,
            mode: read_u32(bytes, 40, "RawFileEntryV1")?,
            mtime: read_u64(bytes, 48, "RawFileEntryV1")?,
            content_sha256: read_array_32(bytes, 56, "RawFileEntryV1")?,
        })
    }

    pub fn to_bytes(&self) -> [u8; RAW_FILE_ENTRY_V1_LEN] {
        let mut bytes = [0u8; RAW_FILE_ENTRY_V1_LEN];
        bytes[0..8].copy_from_slice(&self.path_hash);
        write_u32(&mut bytes, 8, self.path_offset);
        write_u32(&mut bytes, 12, self.path_length);
        write_u64(&mut bytes, 16, self.file_data_size);
        write_u64(&mut bytes, 24, self.first_frame_index);
        write_u64(&mut bytes, 32, self.frame_count);
        write_u32(&mut bytes, 40, self.mode);
        write_u64(&mut bytes, 48, self.mtime);
        bytes[56..88].copy_from_slice(&self.content_sha256);
        bytes
    }
}

#[cfg(test)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RawFrameEntryV1 {
    pub frame_index: u64,
    pub envelope_index: u64,
    pub offset_in_envelope: u32,
    pub compressed_size: u32,
    pub decompressed_size: u32,
    pub raw_stream_offset: u64,
}

#[cfg(test)]
impl RawFrameEntryV1 {
    pub fn parse(bytes: &[u8]) -> Result<Self, FormatError> {
        expect_len("RawFrameEntryV1", RAW_FRAME_ENTRY_V1_LEN, bytes.len())?;
        expect_zero("RawFrameEntryV1", &bytes[28..32])?;
        expect_zero("RawFrameEntryV1", &bytes[40..48])?;
        Ok(Self {
            frame_index: read_u64(bytes, 0, "RawFrameEntryV1")?,
            envelope_index: read_u64(bytes, 8, "RawFrameEntryV1")?,
            offset_in_envelope: read_u32(bytes, 16, "RawFrameEntryV1")?,
            compressed_size: read_u32(bytes, 20, "RawFrameEntryV1")?,
            decompressed_size: read_u32(bytes, 24, "RawFrameEntryV1")?,
            raw_stream_offset: read_u64(bytes, 32, "RawFrameEntryV1")?,
        })
    }

    pub fn to_bytes(&self) -> [u8; RAW_FRAME_ENTRY_V1_LEN] {
        let mut bytes = [0u8; RAW_FRAME_ENTRY_V1_LEN];
        write_u64(&mut bytes, 0, self.frame_index);
        write_u64(&mut bytes, 8, self.envelope_index);
        write_u32(&mut bytes, 16, self.offset_in_envelope);
        write_u32(&mut bytes, 20, self.compressed_size);
        write_u32(&mut bytes, 24, self.decompressed_size);
        write_u64(&mut bytes, 32, self.raw_stream_offset);
        bytes
    }
}

#[cfg(test)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RawEnvelopeEntryV1 {
    pub envelope_index: u64,
    pub first_block_index: u64,
    pub data_block_count: u32,
    pub parity_block_count: u32,
    pub encrypted_size: u32,
    pub plaintext_size: u32,
    pub first_frame_index: u64,
    pub frame_count: u64,
}

#[cfg(test)]
impl RawEnvelopeEntryV1 {
    pub fn parse(bytes: &[u8]) -> Result<Self, FormatError> {
        expect_len("RawEnvelopeEntryV1", RAW_ENVELOPE_ENTRY_V1_LEN, bytes.len())?;
        expect_zero("RawEnvelopeEntryV1", &bytes[48..56])?;
        Ok(Self {
            envelope_index: read_u64(bytes, 0, "RawEnvelopeEntryV1")?,
            first_block_index: read_u64(bytes, 8, "RawEnvelopeEntryV1")?,
            data_block_count: read_u32(bytes, 16, "RawEnvelopeEntryV1")?,
            parity_block_count: read_u32(bytes, 20, "RawEnvelopeEntryV1")?,
            encrypted_size: read_u32(bytes, 24, "RawEnvelopeEntryV1")?,
            plaintext_size: read_u32(bytes, 28, "RawEnvelopeEntryV1")?,
            first_frame_index: read_u64(bytes, 32, "RawEnvelopeEntryV1")?,
            frame_count: read_u64(bytes, 40, "RawEnvelopeEntryV1")?,
        })
    }

    pub fn to_bytes(&self) -> [u8; RAW_ENVELOPE_ENTRY_V1_LEN] {
        let mut bytes = [0u8; RAW_ENVELOPE_ENTRY_V1_LEN];
        write_u64(&mut bytes, 0, self.envelope_index);
        write_u64(&mut bytes, 8, self.first_block_index);
        write_u32(&mut bytes, 16, self.data_block_count);
        write_u32(&mut bytes, 20, self.parity_block_count);
        write_u32(&mut bytes, 24, self.encrypted_size);
        write_u32(&mut bytes, 28, self.plaintext_size);
        write_u64(&mut bytes, 32, self.first_frame_index);
        write_u64(&mut bytes, 40, self.frame_count);
        bytes
    }
}

#[cfg(test)]
fn expect_len(structure: &'static str, expected: usize, actual: usize) -> Result<(), FormatError> {
    if actual != expected {
        return Err(FormatError::InvalidLength {
            structure,
            expected,
            actual,
        });
    }
    Ok(())
}

#[cfg(test)]
fn expect_magic(
    structure: &'static str,
    expected: [u8; 8],
    actual: &[u8],
) -> Result<(), FormatError> {
    if actual != expected {
        return Err(FormatError::BadMagic { structure });
    }
    Ok(())
}

#[cfg(test)]
fn expect_zero(structure: &'static str, bytes: &[u8]) -> Result<(), FormatError> {
    if bytes.iter().any(|byte| *byte != 0) {
        return Err(FormatError::NonZeroReserved { structure });
    }
    Ok(())
}

#[cfg(test)]
fn read_array_8(
    bytes: &[u8],
    offset: usize,
    structure: &'static str,
) -> Result<[u8; 8], FormatError> {
    bytes
        .get(offset..offset + 8)
        .ok_or(FormatError::InvalidLength {
            structure,
            expected: offset + 8,
            actual: bytes.len(),
        })?
        .try_into()
        .map_err(|_| FormatError::InvalidLength {
            structure,
            expected: offset + 8,
            actual: bytes.len(),
        })
}

#[cfg(test)]
fn read_array_32(
    bytes: &[u8],
    offset: usize,
    structure: &'static str,
) -> Result<[u8; 32], FormatError> {
    bytes
        .get(offset..offset + 32)
        .ok_or(FormatError::InvalidLength {
            structure,
            expected: offset + 32,
            actual: bytes.len(),
        })?
        .try_into()
        .map_err(|_| FormatError::InvalidLength {
            structure,
            expected: offset + 32,
            actual: bytes.len(),
        })
}

#[cfg(test)]
fn read_u16(bytes: &[u8], offset: usize, structure: &'static str) -> Result<u16, FormatError> {
    let raw = bytes
        .get(offset..offset + 2)
        .ok_or(FormatError::InvalidLength {
            structure,
            expected: offset + 2,
            actual: bytes.len(),
        })?;
    Ok(u16::from_le_bytes(raw.try_into().map_err(|_| {
        FormatError::InvalidLength {
            structure,
            expected: offset + 2,
            actual: bytes.len(),
        }
    })?))
}

#[cfg(test)]
fn read_u32(bytes: &[u8], offset: usize, structure: &'static str) -> Result<u32, FormatError> {
    let raw = bytes
        .get(offset..offset + 4)
        .ok_or(FormatError::InvalidLength {
            structure,
            expected: offset + 4,
            actual: bytes.len(),
        })?;
    Ok(u32::from_le_bytes(raw.try_into().map_err(|_| {
        FormatError::InvalidLength {
            structure,
            expected: offset + 4,
            actual: bytes.len(),
        }
    })?))
}

#[cfg(test)]
fn read_u64(bytes: &[u8], offset: usize, structure: &'static str) -> Result<u64, FormatError> {
    let raw = bytes
        .get(offset..offset + 8)
        .ok_or(FormatError::InvalidLength {
            structure,
            expected: offset + 8,
            actual: bytes.len(),
        })?;
    Ok(u64::from_le_bytes(raw.try_into().map_err(|_| {
        FormatError::InvalidLength {
            structure,
            expected: offset + 8,
            actual: bytes.len(),
        }
    })?))
}

#[cfg(test)]
fn write_u16(bytes: &mut [u8], offset: usize, value: u16) {
    bytes[offset..offset + 2].copy_from_slice(&value.to_le_bytes());
}

#[cfg(test)]
fn write_u32(bytes: &mut [u8], offset: usize, value: u32) {
    bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
}

#[cfg(test)]
fn write_u64(bytes: &mut [u8], offset: usize, value: u64) {
    bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
}

#[cfg(test)]
mod tests {
    use std::io::Cursor;

    use crate::crypto::{compute_hmac, HmacDomain, MasterKey, Subkeys};
    use crate::format::{
        AeadAlgo, CompressionAlgo, FecAlgo, KdfAlgo, CRYPTO_EXTENSION_HEADER_LEN,
        CRYPTO_HEADER_FIXED_LEN, CRYPTO_HEADER_HMAC_LEN, FORMAT_VERSION, VOLUME_FORMAT_REV,
        VOLUME_HEADER_LEN, VOLUME_TRAILER_LEN,
    };
    use crate::non_seekable_reader::{
        verify_non_seekable_stream, verify_non_seekable_stream_with_bootstrap_sidecar,
        NonSeekableReaderOptions,
    };
    use crate::reader::{
        open_archive, open_archive_with_bootstrap_sidecar, open_non_seekable_archive,
        public_no_key_verify_archive_with, sequential_extract_tar_stream,
    };
    use crate::tar_model::parse_tar_member_group;
    use crate::wire::{
        validate_crypto_extension_semantics, CryptoHeaderFixed, ExtensionTlv, VolumeHeader,
    };

    use super::*;

    #[test]
    fn raw_stream_index_rows_round_trip() {
        let root = RawStreamIndexRootV1 {
            file_count: 1,
            frame_count: 2,
            envelope_count: 3,
            payload_block_count: 4,
            raw_total_size: 5,
            raw_content_sha256: [6; 32],
            raw_file_entry_offset: RAW_STREAM_INDEX_ROOT_V1_LEN as u32,
            raw_frame_table_offset: 200,
            raw_envelope_table_offset: 300,
            string_pool_offset: 400,
            string_pool_size: 12,
        };
        assert_eq!(RawStreamIndexRootV1::parse(&root.to_bytes()).unwrap(), root);

        let file = RawFileEntryV1 {
            path_hash: [1; 8],
            path_offset: 400,
            path_length: 15,
            file_data_size: 1234,
            first_frame_index: 0,
            frame_count: 2,
            mode: 0o644,
            mtime: 1_700_000_000,
            content_sha256: [2; 32],
        };
        assert_eq!(RawFileEntryV1::parse(&file.to_bytes()).unwrap(), file);

        let frame = RawFrameEntryV1 {
            frame_index: 7,
            envelope_index: 8,
            offset_in_envelope: 9,
            compressed_size: 10,
            decompressed_size: 11,
            raw_stream_offset: 12,
        };
        assert_eq!(RawFrameEntryV1::parse(&frame.to_bytes()).unwrap(), frame);

        let envelope = RawEnvelopeEntryV1 {
            envelope_index: 13,
            first_block_index: 14,
            data_block_count: 15,
            parity_block_count: 16,
            encrypted_size: 17,
            plaintext_size: 18,
            first_frame_index: 19,
            frame_count: 20,
        };
        assert_eq!(
            RawEnvelopeEntryV1::parse(&envelope.to_bytes()).unwrap(),
            envelope
        );
    }

    #[test]
    fn raw_content_model_extension_is_critical_and_exact() {
        let tlv = serialize_raw_stream_content_model_extension();
        assert_eq!(
            u16::from_le_bytes(tlv[0..2].try_into().unwrap()),
            RAW_STREAM_CONTENT_MODEL_EXTENSION_CRITICAL_TAG
        );
        assert_eq!(
            u32::from_le_bytes(tlv[2..6].try_into().unwrap()),
            RAW_STREAM_CONTENT_MODEL_VALUE.len() as u32
        );
        assert_eq!(&tlv[6..], RAW_STREAM_CONTENT_MODEL_VALUE);

        assert_eq!(
            validate_raw_stream_content_model_extension(false, RAW_STREAM_CONTENT_MODEL_VALUE)
                .unwrap_err(),
            FormatError::MalformedKnownExtension(RAW_STREAM_CONTENT_MODEL_EXTENSION_TAG)
        );
        assert_eq!(
            validate_raw_stream_content_model_extension(true, b"tar_member_v41").unwrap_err(),
            FormatError::MalformedKnownExtension(RAW_STREAM_CONTENT_MODEL_EXTENSION_TAG)
        );
    }

    #[test]
    fn base_v41_readers_reject_raw_stream_profile_before_tar_metadata() {
        let master_key = MasterKey::from_raw_key(&[9; 32]).unwrap();
        let archive = minimal_raw_profile_archive(&master_key);
        let expected = FormatError::ReaderUnsupported(RAW_STREAM_UNSUPPORTED_MESSAGE);

        assert_eq!(open_archive(&archive, &master_key).unwrap_err(), expected);
        assert_eq!(
            sequential_extract_tar_stream(&archive, &master_key).unwrap_err(),
            expected
        );
        assert_eq!(
            verify_non_seekable_stream(Cursor::new(&archive), &master_key).unwrap_err(),
            expected
        );
        assert_eq!(
            open_archive_with_bootstrap_sidecar(&archive, b"not a sidecar", &master_key)
                .unwrap_err(),
            expected
        );
        assert_eq!(
            open_non_seekable_archive(&archive, &master_key, Some(b"not a sidecar")).unwrap_err(),
            expected
        );
        assert_eq!(
            verify_non_seekable_stream_with_bootstrap_sidecar(
                Cursor::new(&archive),
                b"not a sidecar",
                &master_key,
                NonSeekableReaderOptions::default(),
            )
            .unwrap_err(),
            expected
        );
        assert_eq!(
            public_no_key_verify_archive_with(&archive, |_, _| Ok(true)).unwrap_err(),
            expected
        );
    }

    #[test]
    fn raw_stream_profile_rejects_before_tar_metadata_poison_corpus() {
        let master_key = MasterKey::from_raw_key(&[9; 32]).unwrap();
        let expected = FormatError::ReaderUnsupported(RAW_STREAM_UNSUPPORTED_MESSAGE);

        let mut bad_checksum = tar_member(b"file.txt", b'0', b"abc", b"");
        bad_checksum[0] = b'F';

        let mut nonzero_padding = tar_member(b"file.txt", b'0', b"a", b"");
        nonzero_padding[513] = 1;

        let mut pax_size_exceeds_group =
            tar_member(b"PaxHeaders/file", b'x', &pax_record("size", b"4096"), b"");
        pax_size_exceeds_group.extend_from_slice(&tar_member_with_declared_size(
            b"file", b'0', 0, b"short", b"",
        ));

        let metadata_only = tar_member(
            b"PaxHeaders/file",
            b'x',
            &pax_record("path", b"safe.txt"),
            b"",
        );

        let poison_cases = vec![
            (
                "global pax",
                tar_member(b"global", b'g', &pax_record("path", b"poisoned.txt"), b""),
                FormatError::InvalidArchive("global PAX headers are not allowed"),
            ),
            (
                "gnu sparse entry",
                tar_member(b"sparse.bin", b'S', b"", b""),
                FormatError::ReaderUnsupported("unsupported GNU sparse tar entry"),
            ),
            (
                "unsupported typeflag",
                tar_member(b"fifo", b'6', b"", b""),
                FormatError::ReaderUnsupported("unsupported tar entry type"),
            ),
            (
                "unsafe absolute path",
                tar_member(b"/absolute", b'0', b"abc", b""),
                FormatError::UnsafeArchivePath,
            ),
            (
                "bad checksum",
                bad_checksum,
                FormatError::InvalidArchive("tar header checksum mismatch"),
            ),
            (
                "nonzero padding",
                nonzero_padding,
                FormatError::InvalidArchive("tar member padding is non-zero"),
            ),
            (
                "pax size exceeds group",
                pax_size_exceeds_group,
                FormatError::InvalidLength {
                    structure: "tar member",
                    expected: 5632,
                    actual: 2048,
                },
            ),
            (
                "metadata without main entry",
                metadata_only,
                FormatError::InvalidArchive(
                    "tar member group has metadata records but no main entry",
                ),
            ),
        ];

        for (name, tar_body, tar_error) in poison_cases {
            assert_eq!(
                parse_tar_member_group(&tar_body, 4096).unwrap_err(),
                tar_error,
                "{name}"
            );

            let archive = minimal_raw_profile_archive_with_body(&master_key, &tar_body);
            assert_eq!(
                open_archive(&archive, &master_key).unwrap_err(),
                expected,
                "{name}: seekable open"
            );
            assert_eq!(
                sequential_extract_tar_stream(&archive, &master_key).unwrap_err(),
                expected,
                "{name}: sequential extraction"
            );
            assert_eq!(
                verify_non_seekable_stream(Cursor::new(&archive), &master_key).unwrap_err(),
                expected,
                "{name}: non-seekable verify"
            );
            assert_eq!(
                public_no_key_verify_archive_with(&archive, |_, _| Ok(true)).unwrap_err(),
                expected,
                "{name}: public no-key verify"
            );
        }
    }

    #[test]
    fn non_critical_raw_stream_tag_is_forward_compatible_unknown_extension() {
        let extension = ExtensionTlv {
            tag: RAW_STREAM_CONTENT_MODEL_EXTENSION_TAG,
            value: b"ignored by base v41",
        };

        validate_crypto_extension_semantics(std::slice::from_ref(&extension)).unwrap();
        assert_eq!(
            content_profile_from_extensions(&[extension]).unwrap(),
            ContentProfile::TarMemberV41
        );
    }

    fn minimal_raw_profile_archive(master_key: &MasterKey) -> Vec<u8> {
        minimal_raw_profile_archive_with_body(master_key, &[])
    }

    fn minimal_raw_profile_archive_with_body(master_key: &MasterKey, body: &[u8]) -> Vec<u8> {
        let archive_uuid = [1; 16];
        let session_id = [2; 16];
        let subkeys = Subkeys::derive(master_key, &archive_uuid, &session_id).unwrap();
        let extension = serialize_raw_stream_content_model_extension();
        let crypto_len = CRYPTO_HEADER_FIXED_LEN
            + 2
            + extension.len()
            + CRYPTO_EXTENSION_HEADER_LEN
            + CRYPTO_HEADER_HMAC_LEN;
        let fixed = CryptoHeaderFixed {
            length: crypto_len as u32,
            compression_algo: CompressionAlgo::ZstdFramed,
            aead_algo: AeadAlgo::AesGcmSiv256,
            fec_algo: FecAlgo::ReedSolomonGF16,
            kdf_algo: KdfAlgo::Raw,
            chunk_size: 4096,
            envelope_target_size: 4096,
            block_size: 4096,
            fec_data_shards: 1,
            fec_parity_shards: 0,
            index_fec_data_shards: 1,
            index_fec_parity_shards: 0,
            index_root_fec_data_shards: 1,
            index_root_fec_parity_shards: 0,
            stripe_width: 1,
            volume_loss_tolerance: 0,
            bit_rot_buffer_pct: 0,
            has_dictionary: 0,
            max_path_length: 4096,
            expected_volume_size: 0,
        };
        let mut crypto = fixed.to_bytes().to_vec();
        crypto.extend_from_slice(&(KdfAlgo::Raw as u16).to_le_bytes());
        crypto.extend_from_slice(&extension);
        crypto.extend_from_slice(&0u16.to_le_bytes());
        crypto.extend_from_slice(&0u32.to_le_bytes());
        let hmac = compute_hmac(
            HmacDomain::CryptoHeader,
            &subkeys.mac_key,
            &archive_uuid,
            &session_id,
            &crypto,
        );
        crypto.extend_from_slice(&hmac);

        let header = VolumeHeader {
            format_version: FORMAT_VERSION,
            volume_format_rev: VOLUME_FORMAT_REV,
            volume_index: 0,
            stripe_width: 1,
            archive_uuid,
            session_id,
            crypto_header_offset: VOLUME_HEADER_LEN as u32,
            crypto_header_length: crypto.len() as u32,
            header_crc32c: 0,
        };
        let mut archive = header.to_bytes().to_vec();
        archive.extend_from_slice(&crypto);
        archive.extend_from_slice(body);
        archive.resize(
            VOLUME_HEADER_LEN + crypto.len() + body.len() + VOLUME_TRAILER_LEN,
            0,
        );
        archive
    }

    fn tar_header(path: &[u8], kind: u8, size: usize, link: &[u8]) -> [u8; 512] {
        let mut header = [0u8; 512];
        header[..path.len()].copy_from_slice(path);
        write_octal(&mut header[100..108], 0o644);
        write_octal(&mut header[108..116], 0);
        write_octal(&mut header[116..124], 0);
        write_octal(&mut header[124..136], size as u64);
        write_octal(&mut header[136..148], 0);
        header[148..156].fill(b' ');
        header[156] = kind;
        header[157..157 + link.len()].copy_from_slice(link);
        header[257..263].copy_from_slice(b"ustar\0");
        header[263..265].copy_from_slice(b"00");
        let checksum = header.iter().map(|byte| *byte as u64).sum::<u64>();
        write_checksum(&mut header[148..156], checksum);
        header
    }

    fn tar_member(path: &[u8], kind: u8, data: &[u8], link: &[u8]) -> Vec<u8> {
        tar_member_with_declared_size(path, kind, data.len(), data, link)
    }

    fn tar_member_with_declared_size(
        path: &[u8],
        kind: u8,
        declared_size: usize,
        data: &[u8],
        link: &[u8],
    ) -> Vec<u8> {
        let mut out = Vec::new();
        out.extend_from_slice(&tar_header(path, kind, declared_size, link));
        out.extend_from_slice(data);
        out.resize(out.len() + tar_padding_to_512(data.len()), 0);
        out
    }

    fn pax_record(key: &str, value: &[u8]) -> Vec<u8> {
        let mut len = key.len() + value.len() + 4;
        loop {
            let candidate = len.to_string().len() + 1 + key.len() + 1 + value.len() + 1;
            if candidate == len {
                break;
            }
            len = candidate;
        }
        let mut out = Vec::new();
        out.extend_from_slice(len.to_string().as_bytes());
        out.push(b' ');
        out.extend_from_slice(key.as_bytes());
        out.push(b'=');
        out.extend_from_slice(value);
        out.push(b'\n');
        out
    }

    fn write_octal(field: &mut [u8], value: u64) {
        let digits = format!("{value:o}");
        field.fill(0);
        let start = field.len() - 1 - digits.len();
        field[..start].fill(b'0');
        field[start..start + digits.len()].copy_from_slice(digits.as_bytes());
    }

    fn write_checksum(field: &mut [u8], value: u64) {
        let digits = format!("{value:06o}");
        field[0..6].copy_from_slice(digits.as_bytes());
        field[6] = 0;
        field[7] = b' ';
    }

    fn tar_padding_to_512(len: usize) -> usize {
        let remainder = len % 512;
        if remainder == 0 {
            0
        } else {
            512 - remainder
        }
    }
}
