// Copyright 2022 Adobe. All rights reserved.
// This file is licensed to you under the Apache License,
// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
// or the MIT license (http://opensource.org/licenses/MIT),
// at your option.

// Unless required by applicable law or agreed to in writing,
// this software is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
// implied. See the LICENSE-MIT and LICENSE-APACHE files for thema
// specific language governing permissions and limitations under
// each license.

#![allow(clippy::unwrap_used)]

#[cfg(feature = "file_io")]
use std::path::Path;
use std::{
    collections::HashMap,
    io::{Cursor, Read, Write},
    path::PathBuf,
    sync::LazyLock,
};

use env_logger;
use tempfile::TempDir;

use crate::{
    assertions::{
        labels, Action, Actions, DigitalSourceType, EmbeddedData, Ingredient, Relationship,
        ReviewRating, Thumbnail, User,
    },
    asset_io::CAIReadWrite,
    claim::Claim,
    context::Context,
    crypto::cose::CertificateTrustPolicy,
    hash_utils::Hasher,
    jumbf_io::get_assetio_handler,
    resource_store::UriOrResource,
    store::Store,
    utils::{io_utils::tempdirectory, mime::extension_to_mime},
    AsyncSigner, ClaimGeneratorInfo, Result, SigningAlg,
};

pub const TEST_SMALL_JPEG: &str = "earth_apollo17.jpg";

pub const TEST_WEBP: &str = "mars.webp";

pub const TEST_USER_ASSERTION: &str = "test_label";

/// File extension for external manifest sidecar files
pub const MANIFEST_STORE_EXT: &str = "c2pa";

pub const TEST_VC: &str = r#"{
    "@context": [
    "https://www.w3.org/2018/credentials/v1",
    "http://schema.org"
    ],
    "type": [
    "VerifiableCredential",
    "NPPACredential"
    ],
    "issuer": "https://nppa.org/",
    "credentialSubject": {
        "id": "did:nppa:eb1bb9934d9896a374c384521410c7f14",
        "name": "Bob Ross",
        "memberOf": "https://nppa.org/"
    },
    "proof": {
        "type": "RsaSignature2018",
        "created": "2021-06-18T21:19:10Z",
        "proofPurpose": "assertionMethod",
        "verificationMethod":
        "did:nppa:eb1bb9934d9896a374c384521410c7f14#_Qq0UL2Fq651Q0Fjd6TvnYE-faHiOpRlPVQcY_-tA4A",
        "jws": "eyJhbGciOiJQUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19DJBMvvFAIC00nSGB6Tn0XKbbF9XrsaJZREWvR2aONYTQQxnyXirtXnlewJMBBn2h9hfcGZrvnC1b6PgWmukzFJ1IiH1dWgnDIS81BH-IxXnPkbuYDeySorc4QU9MJxdVkY5EL4HYbcIfwKj6X4LBQ2_ZHZIu1jdqLcRZqHcsDF5KKylKc1THn5VRWy5WhYg_gBnyWny8E6Qkrze53MR7OuAmmNJ1m1nN8SxDrG6a08L78J0-Fbas5OjAQz3c17GY8mVuDPOBIOVjMEghBlgl3nOi1ysxbRGhHLEK4s0KKbeRogZdgt1DkQxDFxxn41QWDw_mmMCjs9qxg0zcZzqEJw"
    }
}"#;

// Macro that both defines constants and registers fixtures
macro_rules! define_fixtures {
    ($base_path:expr, $($name:ident => ($file:expr, $format:expr)),* $(,)?) => {
        // Define the constants
        $(
            pub const $name: &str = $file;
        )*

        // Create the registry mapping filenames to data and format
        static EMBEDDED_FIXTURES: LazyLock<HashMap<&'static str, (&'static [u8], &'static str)>> = LazyLock::new(|| {
            let mut map = HashMap::new();
            $(
                // Convert to &[u8] slice to avoid fixed-size array type issues
                let bytes: &'static [u8] = include_bytes!(concat!("../../tests/fixtures/", $file));
                map.insert($file, (bytes, $format));
            )*
            map
        });

        // Add a registry access function
        pub fn get_registry() -> &'static HashMap<&'static str, (&'static [u8], &'static str)> {
            &EMBEDDED_FIXTURES
        }
    };
}

// Register your fixtures with the macro
// Use with base path parameter
define_fixtures!(
    "../../tests/fixtures/",
    SMALL_JPEG => ("earth_apollo17.jpg", "image/jpeg"),
    C_JPEG => ("C.jpg", "image/jpeg"),
    CA_JPEG => ("CA.jpg", "image/jpeg"),
    XCA_JPEG => ("XCA.jpg", "image/jpeg"),
    SAMPLE_PNG => ("sample1.png", "image/png"),
    SAMPLE_WAV => ("sample1.wav", "audio/wav"),
    SAMPLE_WEBP => ("sample1.webp", "image/webp"),
    SAMPLE_TIFF => ("TUSCANY.TIF", "image/tiff"),
    SAMPLE_AVI => ("test.avi", "video/avi"),
    SAMPLE_AVIF => ("sample1.avif", "image/avif"),
    SAMPLE_HEIC => ("sample1.heic", "image/heic"),
    SAMPLE_HEIF => ("sample1.heif", "image/heif"),
    SAMPLE_MP4 => ("video1.mp4", "video/mp4"),
    LEGACY_MP4 => ("legacy.mp4", "video/mp4"),
    NO_MANIFEST_MP4 => ("video1_no_manifest.mp4", "video/mp4"),
    LEGACY_INGREDIENT_HASH => ("legacy_ingredient_hash.jpg", "image/jpeg"),
    NO_MANIFEST => ("no_manifest.jpg", "image/jpeg"),
    NO_ALG => ("no_alg.jpg", "image/jpeg"),
    SAMPLE_BAD_SIGNATURE => ("CIE-sig-CA.jpg", "image/jpeg"),
    SAMPLE_PSD => ("Purple Square.psd", "image/vnd.adobe.photoshop"),
    TEST_TEXT_PLAIN => ("unsupported_type.txt", "text/plain"),
    PRE_RELEASE => ("prerelease.jpg", "image/jpeg"),
    C_MOV => ("c.mov", "video/quicktime"),

    // Add more as needed
);

pub fn setup_logger() {
    static INIT: std::sync::Once = std::sync::Once::new();
    INIT.call_once(|| {
        let _ = env_logger::builder().is_test(true).try_init();
    });
}

/// Returns Settings configured for testing.
///
/// This loads the standard test settings from `tests/fixtures/test_settings.toml`,
/// which includes trust anchors, signer configuration, and verification settings
/// appropriate for testing.
///
/// # Panics
///
/// Panics if test settings cannot be loaded.
///
/// # Examples
///
/// ```rust,ignore
/// use crate::utils::test::test_settings;
///
/// // Use directly with Context
/// let context = Context::new().with_settings(test_settings())?;
///
/// // Or modify for specific test needs
/// let mut settings = test_settings();
/// settings.verify.verify_trust = false;
/// let context = Context::new().with_settings(settings)?;
/// ```
#[allow(clippy::expect_used)]
pub fn test_settings() -> crate::Settings {
    crate::Settings::new()
        .with_toml(include_str!("../../tests/fixtures/test_settings.toml"))
        .expect("built-in test_settings.toml should be valid")
}

/// Creates a Context configured with standard test settings.
///
/// This is equivalent to `Context::new().with_settings(test_settings())`.
/// Use this for most tests that need a configured context.
///
/// # Panics
///
/// Panics if test settings cannot be loaded.
///
/// # Examples
///
/// ```rust,ignore
/// use crate::utils::test::test_context;
///
/// // Single use
/// let builder = Builder::from_context(test_context());
///
/// // Shared across multiple components
/// let ctx = test_context().into_shared();
/// let builder1 = Builder::from_shared_context(&ctx);
/// let builder2 = Builder::from_shared_context(&ctx);
/// ```
#[allow(clippy::expect_used)]
pub fn test_context() -> Context {
    Context::new()
        .with_settings(test_settings())
        .expect("test_settings should always be valid")
}

/// Create new C2PA compatible UUID
pub(crate) fn gen_c2pa_uuid() -> String {
    let guid = uuid::Uuid::new_v4();
    guid.hyphenated()
        .encode_lower(&mut uuid::Uuid::encode_buffer())
        .to_owned()
}

// Returns a non-changing C2PA compatible UUID for testing
pub(crate) fn static_test_v1_uuid() -> &'static str {
    const TEST_GUID: &str = "urn:uuid:f75ddc48-cdc8-4723-bcfe-77a8d68a5920";
    TEST_GUID
}

/// Creates a minimal valid claim for testing (v2)
///
/// This claim has just enough information to be valid, including a
/// claim_generator_info and a c2pa.created action assertion.
pub fn create_min_test_claim() -> Result<Claim> {
    let mut claim = Claim::new("contentauth unit test", Some("contentauth"), 2);

    let mut cg_info = ClaimGeneratorInfo::new("test app");
    cg_info.version = Some("2.3.4".to_string());
    claim.add_claim_generator_info(cg_info);

    let created_action = Action::new("c2pa.created").set_source_type(DigitalSourceType::Empty);
    let actions = Actions::new().add_action(created_action);

    claim.add_assertion(&actions)?;

    Ok(claim)
}

/// Creates a claim for testing (v2)
pub fn create_test_claim() -> Result<Claim> {
    let mut claim = Claim::new("contentauth unit test", Some("contentauth"), 2);

    // Add an icon for the claim_generator
    let icon = EmbeddedData::new(labels::ICON, "image/jpeg", vec![0xde, 0xad, 0xbe, 0xef]);
    let icon_ref = claim.add_assertion(&icon)?;

    let mut cg_info = ClaimGeneratorInfo::new("test app");
    cg_info.version = Some("2.3.4".to_string());
    cg_info.icon = Some(UriOrResource::HashedUri(icon_ref));
    cg_info.insert("something", "else");

    claim.add_claim_generator_info(cg_info);

    // Create a thumbnail for the claim
    let claim_thumbnail = EmbeddedData::new(
        labels::CLAIM_THUMBNAIL,
        "image/jpeg",
        vec![0xde, 0xad, 0xbe, 0xef],
    );
    let _claim_thumbnail_ref = claim.add_assertion(&claim_thumbnail)?;

    // Create and add a thumbnail for an ingredient
    let ingredient_thumbnail = EmbeddedData::new(
        labels::INGREDIENT_THUMBNAIL,
        "image/jpeg",
        vec![0xde, 0xad, 0xbe, 0xef],
    );
    let ingredient_thumbnail_ref = claim.add_assertion(&ingredient_thumbnail)?;

    // create a new v3 ingredient and add the thumbnail reference
    let ingredient = Ingredient::new_v3(Relationship::ComponentOf)
        .set_title("image_1.jpg")
        .set_format("image/jpeg")
        .set_thumbnail(Some(&ingredient_thumbnail_ref));
    let ingredient_ref = claim.add_assertion(&ingredient)?;

    // create a second v3 ingredient and add the thumbnail reference
    let ingredient2 = Ingredient::new_v3(Relationship::ComponentOf)
        .set_title("image_2.jpg")
        .set_format("image/png")
        .set_thumbnail(Some(&ingredient_thumbnail_ref));
    let ingredient_ref2 = claim.add_assertion(&ingredient2)?;

    let created_action = Action::new("c2pa.created").set_source_type(DigitalSourceType::Empty);

    let placed_action = Action::new("c2pa.placed")
        .set_parameter("ingredients", vec![ingredient_ref, ingredient_ref2])?;

    // Add assertions.
    let actions = Actions::new()
        .add_action(created_action)
        .add_action(placed_action);

    claim.add_assertion(&actions)?;

    Ok(claim)
}

/// creates a claim for testing (v1)
pub fn create_test_claim_v1() -> Result<Claim> {
    let mut claim = Claim::new("adobe unit test", Some("adobe"), 1);

    // add some data boxes
    let _db_uri = claim.add_databox("text/plain", "this is a test".as_bytes().to_vec(), None)?;
    let _db_uri_1 =
        claim.add_databox("text/plain", "this is more text".as_bytes().to_vec(), None)?;

    // add VC entry
    let _hu = claim.add_verifiable_credential(TEST_VC)?;

    // Add assertions.
    let actions = Actions::new()
        .add_action(Action::new("c2pa.created"))
        .add_action(
            Action::new("c2pa.cropped")
                .set_parameter(
                    "name".to_owned(),
                    r#"{"left": 0, "right": 2000, "top": 1000, "bottom": 4000}"#,
                )
                .unwrap(),
        )
        .add_action(
            Action::new("c2pa.filtered")
                .set_parameter("name".to_owned(), "gaussian blur")?
                .set_when("2015-06-26T16:43:23+0200"),
        );
    // add a binary thumbnail assertion  ('deadbeefadbeadbe')
    let some_binary_data: Vec<u8> = vec![
        0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f, 0x0a, 0x0d, 0x0b, 0x0e, 0x0a, 0x0d, 0x0b,
        0x0e,
    ];

    let user_assertion_data = r#"{
        "test_label": "test_value"
    }"#;

    // create a schema.org claim
    let cr = r#"{
        "@context": "https://schema.org",
        "@type": "ClaimReview",
        "claimReviewed": "The world is flat",
        "reviewRating": {
            "@type": "Rating",
            "ratingValue": "1",
            "bestRating": "5",
            "worstRating": "1",
            "alternateName": "False"
        }
    }"#;
    let claim_review = User::new("schema.org", cr);
    let thumbnail_claim = Thumbnail::new(labels::JPEG_CLAIM_THUMBNAIL, some_binary_data.clone());
    let thumbnail_ingred = Thumbnail::new(labels::JPEG_INGREDIENT_THUMBNAIL, some_binary_data);
    let user_assertion = User::new(TEST_USER_ASSERTION, user_assertion_data);

    claim.add_assertion(&actions)?;
    claim.add_assertion(&claim_review)?;
    claim.add_assertion(&thumbnail_claim)?;
    claim.add_assertion(&user_assertion)?;

    let thumb_uri = claim.add_assertion(&thumbnail_ingred)?;

    let review = ReviewRating::new(
        "a 3rd party plugin was used",
        Some("actions.unknownActionsPerformed".to_string()),
        1,
    );

    let ingredient = Ingredient::new(
        "image 1.jpg",
        "image/jpeg",
        "xmp.iid:7b57930e-2f23-47fc-affe-0400d70b738d",
        Some("xmp.did:87d51599-286e-43b2-9478-88c79f49c347"),
    )
    .set_thumbnail(Some(&thumb_uri))
    //.set_manifest_data(&data_path)
    .add_review(review);

    let ingredient2 = Ingredient::new(
        "image 2.png",
        "image/png",
        "xmp.iid:7b57930e-2f23-47fc-affe-0400d70b738c",
        Some("xmp.did:87d51599-286e-43b2-9478-88c79f49c346"),
    )
    .set_thumbnail(Some(&thumb_uri));

    claim.add_assertion(&ingredient)?;
    claim.add_assertion(&ingredient2)?;

    Ok(claim)
}

/// Creates a store with an unsigned claim for testing
pub fn create_test_store() -> Result<Store> {
    // Create claims store.
    let mut store = Store::from_context(&Context::new());

    let claim = create_test_claim()?;
    store.commit_claim(claim).unwrap();
    Ok(store)
}

/// Creates a store with an unsigned v1 claim for testing
pub fn create_test_store_v1() -> Result<Store> {
    // Create claims store.
    let mut store = Store::from_context(&Context::new());

    let claim = create_test_claim_v1()?;
    store.commit_claim(claim).unwrap();
    Ok(store)
}

/// returns a path to a file in the fixtures folder
pub fn fixture_path(file_name: &str) -> PathBuf {
    // File paths are relative to directory specified in dir argument.
    // This assumes `wasmtime --dir .`
    #[cfg(target_os = "wasi")]
    let mut path = PathBuf::from("/");
    #[cfg(not(target_os = "wasi"))]
    let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    path.push("tests/fixtures");
    path.push(file_name);
    path
}

/// Create in-memory test streams from a fixture file
#[allow(clippy::expect_used)]
#[allow(clippy::panic)]
pub fn create_test_streams(
    fixture_name: &str,
) -> (
    &'static str,
    std::io::Cursor<Vec<u8>>,
    std::io::Cursor<Vec<u8>>,
) {
    // Try to use embedded fixture first
    if let Some(fixture) = get_registry().get(fixture_name) {
        // Access tuple elements directly by position
        let data = fixture.0;
        let format = fixture.1;

        let input_cursor = std::io::Cursor::new(data.to_vec());
        let output_cursor = std::io::Cursor::new(Vec::new());

        return (format, input_cursor, output_cursor);
    }

    #[cfg(feature = "file_io")]
    {
        // Fallback to file-based fixture if not embedded
        let input_path = fixture_path(fixture_name);
        let input_data = std::fs::read(&input_path).expect("could not read input file");

        // Determine format from input file extension
        let format = input_path
            .extension()
            .and_then(|ext| ext.to_str())
            .and_then(extension_to_mime)
            .unwrap_or("application/octet-stream");

        let input_cursor = std::io::Cursor::new(input_data);
        let output_cursor = std::io::Cursor::new(Vec::new());

        (format, input_cursor, output_cursor)
    }
    #[cfg(not(feature = "file_io"))]
    {
        panic!(
            "Fixture '{}' not found in embedded registry and file I/O is disabled",
            fixture_name
        );
    }
}

/// Create a single in-memory input stream from a fixture file.
/// Use this for read-only tests (e.g. format detection) that don't need an output stream.
#[allow(clippy::expect_used)]
#[allow(clippy::panic)]
pub fn create_test_stream(fixture_name: &str) -> (&'static str, std::io::Cursor<Vec<u8>>) {
    if let Some(fixture) = get_registry().get(fixture_name) {
        return (fixture.1, std::io::Cursor::new(fixture.0.to_vec()));
    }

    #[cfg(feature = "file_io")]
    {
        let input_path = fixture_path(fixture_name);
        let input_data = std::fs::read(&input_path).expect("could not read input file");
        let format = input_path
            .extension()
            .and_then(|ext| ext.to_str())
            .and_then(extension_to_mime)
            .unwrap_or("application/octet-stream");
        (format, std::io::Cursor::new(input_data))
    }
    #[cfg(not(feature = "file_io"))]
    {
        panic!(
            "Fixture '{}' not found in embedded registry and file I/O is disabled",
            fixture_name
        );
    }
}

/// Setup for file-based tests that need actual file I/O operations
pub struct TestFileSetup {
    pub temp_dir: TempDir,
    pub input_path: PathBuf,
    pub output_path: PathBuf,
    pub format: String,
}

impl TestFileSetup {
    /// Create a new test file setup from a fixture file
    #[allow(clippy::expect_used)]
    pub fn new(fixture_name: &str) -> Self {
        let input_path = fixture_path(fixture_name);
        let extension = input_path
            .extension()
            .and_then(|ext| ext.to_str())
            .unwrap_or("bin");

        let format = extension_to_mime(extension)
            .unwrap_or("application/octet-stream")
            .to_string();
        let temp_dir = tempdirectory().expect("create temp dir");

        // Create output path with same extension as input
        let mut output_path = temp_dir.path().join(fixture_name);
        output_path.set_extension(extension);

        Self {
            temp_dir,
            input_path,
            output_path,
            format,
        }
    }

    /// Get the path to the temporary directory
    pub fn temp_dir_path(&self) -> &std::path::Path {
        self.temp_dir.path()
    }

    /// Create a path within the temporary directory
    pub fn temp_path(&self, filename: &str) -> PathBuf {
        self.temp_dir.path().join(filename)
    }

    /// Get a sidecar path for the output file (with .c2pa extension)
    pub fn sidecar_path(&self) -> PathBuf {
        self.output_path.with_extension(MANIFEST_STORE_EXT)
    }

    /// Create a file:// URL for the sidecar file
    pub fn sidecar_url(&self) -> String {
        let path_buf = self.sidecar_path(); // Store PathBuf in a variable to extend its lifetime
        let path_str = path_buf.to_str().unwrap();
        // Convert backslashes to forward slashes on Windows
        let path_str = path_str.replace('\\', "/");

        // Check if the path already starts with a slash and handle accordingly
        if path_str.starts_with('/') {
            format!("file://{path_str}")
        } else {
            format!("file:///{path_str}")
        }
    }

    /// Get the file extension of the input file
    pub fn extension(&self) -> &str {
        self.input_path
            .extension()
            .and_then(|ext| ext.to_str())
            .unwrap_or("bin")
    }

    /// Create an input file stream for reading
    #[allow(clippy::expect_used)]
    pub fn input_stream(&self) -> std::fs::File {
        std::fs::File::open(&self.input_path).expect("open input file")
    }

    /// Create an output file stream for writing
    #[allow(clippy::expect_used)]
    pub fn output_stream(&self) -> std::fs::File {
        std::fs::OpenOptions::new()
            .create(true)
            .truncate(true)
            .read(true)
            .write(true)
            .open(&self.output_path)
            .expect("create output file")
    }

    /// Create format and streams tuple like create_test_streams
    /// Returns (format, input_stream, output_stream)
    pub fn create_streams(&self) -> (&str, std::fs::File, std::fs::File) {
        (&self.format, self.input_stream(), self.output_stream())
    }
}

/// Run a test that requires file I/O operations
///
/// This helper manages the temporary directory lifecycle and provides
/// the test function with file paths for input and output operations.
pub fn run_file_test<F>(fixture_name: &str, test_fn: F)
where
    F: FnOnce(&TestFileSetup),
{
    let setup = TestFileSetup::new(fixture_name);
    test_fn(&setup);
    // TestFileSetup automatically cleans up temp_dir when dropped
}

/// returns a path to a file in the temp_dir folder
// note, you must pass TempDir from the caller's context
pub fn temp_dir_path(temp_dir: &TempDir, file_name: &str) -> PathBuf {
    temp_dir.path().join(file_name)
}

// copies a fixture to a temp file and returns path to copy
pub fn temp_fixture_path(temp_dir: &TempDir, file_name: &str) -> PathBuf {
    let fixture_src = fixture_path(file_name);
    let fixture_copy = temp_dir_path(temp_dir, file_name);
    std::fs::copy(fixture_src, &fixture_copy).unwrap();
    fixture_copy
}

/// Create a [`Signer`] instance that can be used for testing purposes.
///
/// This is a suitable default for use when you need a [`Signer`], but
/// don't care what the format is.
///
/// # Returns
///
/// Returns a boxed [`Signer`] instance.
///
/// # Panics
///
/// Can panic if the certs cannot be read. (This function should only
/// be used as part of testing infrastructure.)
#[cfg(feature = "file_io")]
pub fn temp_signer_file() -> Box<dyn crate::Signer> {
    #![allow(clippy::expect_used)]
    let mut sign_cert_path = fixture_path("certs");
    sign_cert_path.push("ps256");
    sign_cert_path.set_extension("pub");

    let mut pem_key_path = fixture_path("certs");
    pem_key_path.push("ps256");
    pem_key_path.set_extension("pem");

    crate::create_signer::from_files(&sign_cert_path, &pem_key_path, SigningAlg::Ps256, None)
        .expect("get_temp_signer")
}

/// Create a [`CertificateTrustPolicy`] instance that has the test certificate bundles included.
///
/// [`CertificateTrustPolicy`]: crate::crypto::cose::CertificateTrustPolicy
pub fn test_certificate_acceptance_policy() -> CertificateTrustPolicy {
    let mut ctp = CertificateTrustPolicy::default();
    ctp.add_trust_anchors(include_bytes!(
        "../../tests/fixtures/certs/trust/test_cert_root_bundle.pem"
    ))
    .unwrap();
    ctp
}

#[cfg(feature = "file_io")]
pub fn write_jpeg_placeholder_file(
    placeholder: &[u8],
    input: &Path,
    output_file: &mut dyn CAIReadWrite,
    hasher: Option<&mut Hasher>,
) -> Result<usize> {
    let mut f = std::fs::File::open(input).unwrap();
    write_jpeg_placeholder_stream(placeholder, "jpeg", &mut f, output_file, hasher)
}

/// Utility to create a test file with a placeholder for a manifest
pub fn write_jpeg_placeholder_stream<R>(
    placeholder: &[u8],
    format: &str,
    input: &mut R,
    output_file: &mut dyn CAIReadWrite,
    mut hasher: Option<&mut Hasher>,
) -> Result<usize>
where
    R: Read + std::io::Seek + Send,
{
    let jpeg_io = get_assetio_handler(format).unwrap();
    let box_mapper = jpeg_io.asset_box_hash_ref().unwrap();
    let boxes = box_mapper.get_box_map(input).unwrap();
    let sof = boxes.iter().find(|b| b.names[0] == "SOF0").unwrap();

    // build new asset with hole for new manifest
    let outbuf = Vec::new();
    let mut out_stream = Cursor::new(outbuf);
    input.rewind().unwrap();

    // write before
    let box_len: usize = sof.range_start.try_into()?;
    let mut before = vec![0u8; box_len];
    input.read_exact(before.as_mut_slice()).unwrap();
    if let Some(hasher) = hasher.as_deref_mut() {
        hasher.update(&before);
    }
    out_stream.write_all(&before).unwrap();

    // write placeholder
    out_stream.write_all(placeholder).unwrap();

    // write bytes after
    let mut after_buf = Vec::new();
    input.read_to_end(&mut after_buf).unwrap();
    if let Some(hasher) = hasher {
        hasher.update(&after_buf);
    }
    out_stream.write_all(&after_buf).unwrap();

    // save to output file
    output_file.write_all(&out_stream.into_inner()).unwrap();

    Ok(box_len)
}

/// Utility to create a BMFF (MP4) test asset with a placeholder for a manifest. Note
/// that is not real.  Inserting a box this way will break the MP4 structure, but it
/// is sufficient for testing.
///
/// Inserts `placeholder` (a composed C2PA UUID box, as returned by
/// `Builder::composed_manifest` for BMFF formats) immediately after the `ftyp`
/// box, which is the standard C2PA insertion point in BMFF assets.
///
/// Returns the byte offset where the placeholder was inserted (i.e. the end of
/// the `ftyp` box).
pub fn write_bmff_placeholder_stream<R>(
    placeholder: &[u8],
    input: &mut R,
    output_file: &mut dyn CAIReadWrite,
) -> Result<usize>
where
    R: Read + std::io::Seek + Send,
{
    input.rewind().unwrap();

    // Read the ftyp box header: 4-byte big-endian size + 4-byte box type.
    let mut size_bytes = [0u8; 4];
    input.read_exact(&mut size_bytes).unwrap();
    let mut type_bytes = [0u8; 4];
    input.read_exact(&mut type_bytes).unwrap();
    assert_eq!(
        &type_bytes, b"ftyp",
        "BMFF stream must start with an ftyp box"
    );

    let ftyp_size = u32::from_be_bytes(size_bytes) as usize;

    // Build the output stream with a hole for the manifest.
    let outbuf = Vec::new();
    let mut out_stream = Cursor::new(outbuf);
    input.rewind().unwrap();

    // Copy the ftyp box verbatim.
    let mut before = vec![0u8; ftyp_size];
    input.read_exact(before.as_mut_slice()).unwrap();
    out_stream.write_all(&before).unwrap();

    // Insert the composed placeholder (C2PA UUID box).
    out_stream.write_all(placeholder).unwrap();

    // Copy the remainder of the asset.
    let mut after_buf = Vec::new();
    input.read_to_end(&mut after_buf).unwrap();
    out_stream.write_all(&after_buf).unwrap();

    output_file.write_all(&out_stream.into_inner()).unwrap();

    Ok(ftyp_size)
}

pub(crate) struct TestGoodSigner {}

impl crate::Signer for TestGoodSigner {
    fn sign(&self, _data: &[u8]) -> Result<Vec<u8>> {
        Ok(b"not a valid signature".to_vec())
    }

    fn alg(&self) -> SigningAlg {
        SigningAlg::Ps256
    }

    fn certs(&self) -> Result<Vec<Vec<u8>>> {
        Ok(Vec::new())
    }

    fn reserve_size(&self) -> usize {
        1024
    }

    fn send_timestamp_request(&self, _message: &[u8]) -> Option<crate::error::Result<Vec<u8>>> {
        Some(Ok(Vec::new()))
    }
}

pub(crate) struct AsyncTestGoodSigner {}

#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl AsyncSigner for AsyncTestGoodSigner {
    async fn sign(&self, _data: Vec<u8>) -> Result<Vec<u8>> {
        Ok(b"not a valid signature".to_vec())
    }

    fn alg(&self) -> SigningAlg {
        SigningAlg::Ps256
    }

    fn certs(&self) -> Result<Vec<Vec<u8>>> {
        Ok(Vec::new())
    }

    fn reserve_size(&self) -> usize {
        1024
    }

    async fn send_timestamp_request(
        &self,
        _message: &[u8],
    ) -> Option<crate::error::Result<Vec<u8>>> {
        Some(Ok(Vec::new()))
    }
}

#[test]
fn test_create_test_store() {
    #[allow(clippy::expect_used)]
    let store = create_test_store().expect("create test store");

    assert_eq!(store.claims().len(), 1);
}
