//! Core C2PA manifest generation, signing, and verification logic.
//! Uses ES256 for signing (CAI test assets); no multi-algorithm support in Sprint 1.

#![allow(deprecated)]

use std::io::{BufReader, Read, Seek, SeekFrom};
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};

use anyhow::{ensure, Context, Result};
use c2pa::{
    assertions::{c2pa_action, Action, Actions, CreativeWork, DataHash, SchemaDotOrgPerson},
    create_signer, Builder, BuilderIntent, Reader, Settings, SigningAlg, ValidationState,
};
use rustls_pemfile::certs;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sha2::{Digest, Sha256};

mod ipfs;

pub const PHYSICAL_ANCHOR_ASSERTION_LABEL: &str = "org.trustsystems.physical-anchor.v1";
pub const PHYSICAL_ANCHOR_ASSERTION_LABEL_LEGACY: &str = "org.trustsystems.physical-anchor";
pub const PERCEPTUAL_HASH_ASSERTION_LABEL: &str = "org.trustsystems.perceptual-hash";
pub const ORG_TRUST_ANCHORS_PEM: &str = include_str!("charm_farm_root.pem");
const CLAIM_GENERATOR: &str = "trust-systems/1.25.0";

/// C2PA content-binding assertion label (exclusions define JUMBF ranges to skip).
const C2PA_HASH_DATA_LABEL: &str = "c2pa.hash.data";

/// Abstraction over manifest signing backends (software keys today, TPM later).
#[allow(dead_code)]
pub trait ManifestSigner {
    fn sign(&self, builder: &mut Builder, input: &str, output: &Path) -> Result<()>;
}

/// File-based signer using PEM certificate + private key (Sprint 1 behavior).
#[allow(dead_code)]
pub struct SoftwareSigner {
    material: SoftwareSignerMaterial,
}

enum SoftwareSignerMaterial {
    Files {
        cert_path: String,
        key_path: String,
    },
    InMemory {
        cert_chain_pem: Vec<u8>,
        private_key_pem: Vec<u8>,
    },
}

impl SoftwareSigner {
    pub fn new(cert_path: &str, key_path: &str) -> Result<Self> {
        ensure!(
            !cert_path.trim().is_empty(),
            "certificate path cannot be empty"
        );
        ensure!(!key_path.trim().is_empty(), "key path cannot be empty");
        Ok(Self {
            material: SoftwareSignerMaterial::Files {
                cert_path: cert_path.to_owned(),
                key_path: key_path.to_owned(),
            },
        })
    }

    pub fn from_pem_bytes(cert_chain_pem: &[u8], private_key_pem: &[u8]) -> Result<Self> {
        ensure!(
            !cert_chain_pem.is_empty(),
            "certificate chain cannot be empty"
        );
        ensure!(!private_key_pem.is_empty(), "private key cannot be empty");
        Ok(Self {
            material: SoftwareSignerMaterial::InMemory {
                cert_chain_pem: cert_chain_pem.to_vec(),
                private_key_pem: private_key_pem.to_vec(),
            },
        })
    }
}

impl ManifestSigner for SoftwareSigner {
    fn sign(&self, builder: &mut Builder, input: &str, output: &Path) -> Result<()> {
        let signer = match &self.material {
            SoftwareSignerMaterial::Files {
                cert_path,
                key_path,
            } => create_signer::from_files(cert_path, key_path, SigningAlg::Es256, None),
            SoftwareSignerMaterial::InMemory {
                cert_chain_pem,
                private_key_pem,
            } => create_signer::from_keys(cert_chain_pem, private_key_pem, SigningAlg::Es256, None),
        }
        .context("load signing certificate and key")?;
        builder
            .sign_file(&*signer, input, output)
            .map_err(|e| anyhow::anyhow!("{}", e))?;
        Ok(())
    }
}

/// Draft TPM signer placeholder for future tss-esapi integration.
#[allow(dead_code)]
pub struct TpmSigner;

impl ManifestSigner for TpmSigner {
    fn sign(&self, _builder: &mut Builder, _input: &str, _output: &Path) -> Result<()> {
        Err(anyhow::anyhow!(
            "TPM signer is draft-only in this sprint; use SoftwareSigner"
        ))
    }
}

/// Pluggable physical anchor provider used to generate a handshake hash.
pub trait PhysicalAnchor {
    fn generate_handshake(&self) -> Result<String>;
}

/// Manual hash input for simulating external scanner/NFC handshakes.
pub struct ManualAnchor {
    hash: String,
}

impl ManualAnchor {
    pub fn new(hash: &str) -> Result<Self> {
        let trimmed = hash.trim();
        ensure!(!trimmed.is_empty(), "physical hash cannot be empty");
        ensure!(
            trimmed.len() <= 512,
            "physical hash too long (max 512 characters)"
        );
        Ok(Self {
            hash: trimmed.to_owned(),
        })
    }
}

impl PhysicalAnchor for ManualAnchor {
    fn generate_handshake(&self) -> Result<String> {
        Ok(self.hash.clone())
    }
}

#[derive(Serialize)]
struct PhysicalAnchorAssertion {
    physical_anchor_type: String,
    hash: String,
}

/// NFC tag anchor: uid and chip_family from vault. Used when sealing with a selected tag.
#[derive(Serialize)]
struct PhysicalAnchorAssertionNfc {
    physical_anchor_type: String,
    chip_family: String,
    uid: String,
}

#[derive(Deserialize)]
struct PhysicalAnchorAssertionRead {
    hash: Option<String>,
    #[serde(default)]
    uid: Option<String>,
    #[serde(default)]
    #[allow(dead_code)] // used when deserializing NFC format from manifest
    chip_family: Option<String>,
}

#[derive(Serialize, serde::Deserialize)]
struct PerceptualHashAssertion {
    perceptual_hash: String,
}

/// Validation outcome for a signed asset (spec §3.2).
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[allow(dead_code)] // Success/Failure used once real verification is implemented
pub enum ValidationStatus {
    Success,
    Failure,
    Untrusted,
}

impl std::fmt::Display for ValidationStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ValidationStatus::Success => write!(f, "Success"),
            ValidationStatus::Failure => write!(f, "Failure"),
            ValidationStatus::Untrusted => write!(f, "Untrusted"),
        }
    }
}

/// Result of C2PA manifest verification: status and extracted manifest fields (spec §3.2).
#[derive(Debug, Clone, serde::Serialize)]
pub struct VerificationResult {
    pub status: ValidationStatus,
    /// Human-readable explanation of what the status means.
    pub status_explanation: String,
    pub author: Option<String>,
    /// Manifest title (e.g. "Trust Systems").
    pub title: Option<String>,
    /// Software that created the manifest (claim generator).
    pub claim_generator: Option<String>,
    /// When the manifest was signed (ISO-style string if present).
    pub signed_at: Option<String>,
    /// Signer identity from the certificate (common name or issuer).
    pub signer: Option<String>,
    /// Asset MIME type (e.g. "image/jpeg").
    pub format: Option<String>,
    /// Manifest instance identifier in the store.
    pub instance_id: Option<String>,
    /// Assertion labels present in the manifest (e.g. "stds.schema-org.CreativeWork").
    pub assertion_labels: Vec<String>,
    /// Physical anchor hash extracted from custom assertion, if present.
    pub physical_hash: Option<String>,
    /// Current owner/recipient extracted from CreativeWork.publisher, if present.
    pub publisher: Option<String>,
    /// Publisher identifier metadata from CreativeWork.publisher (self-asserted).
    /// Display-only: do not use this for trust decisions.
    pub publisher_identifier: Option<String>,
    /// SHA256 fingerprint (hex) of the end-entity cert that actually signed the active manifest.
    pub signer_certificate_fingerprint: Option<String>,
    /// Earliest/root manifest author in the chain, if resolvable.
    pub origin_author: Option<String>,
    /// Earliest/root manifest title in the chain, if resolvable.
    pub origin_title: Option<String>,
    /// Earliest/root manifest signing time in the chain, if resolvable.
    pub origin_signed_at: Option<String>,
    /// CreativeWork.description (Schema.org).
    pub description: Option<String>,
    /// CreativeWork.dateCreated (Schema.org).
    pub date_created: Option<String>,
    /// CreativeWork.license (Schema.org).
    pub license: Option<String>,
    /// CreativeWork.keywords (Schema.org).
    pub keywords: Option<String>,
    /// CreativeWork.genre (Schema.org).
    pub genre: Option<String>,
    /// CreativeWork.material (Schema.org).
    pub material: Option<String>,
    /// CreativeWork.contributor (Schema.org) — name extracted from Person.
    pub contributor: Option<String>,
    /// CreativeWork.abstract (Schema.org).
    #[serde(rename = "abstract")]
    pub abstract_: Option<String>,
    /// IPFS CID from CreativeWork.identifier when present (ipfs://{cid}).
    pub ipfs_cid: Option<String>,
    /// Perceptual hash from org.trustsystems.perceptual-hash assertion, if present.
    pub perceptual_hash: Option<String>,
    /// True when the active manifest's signing chain includes the built-in Charm Farm anchor.
    pub is_charmed: bool,
    /// Ordered manifest chain from root -> active for timeline rendering.
    pub manifest_history: Vec<ManifestHistoryEntry>,
}

#[derive(Debug, Clone, serde::Serialize)]
pub struct ManifestHistoryEntry {
    pub label: Option<String>,
    pub instance_id: Option<String>,
    pub title: Option<String>,
    pub author: Option<String>,
    pub publisher: Option<String>,
    pub claim_generator: Option<String>,
    pub signer: Option<String>,
    pub signed_at: Option<String>,
    pub actions: Vec<String>,
    pub assertion_labels: Vec<String>,
    pub is_active: bool,
    pub is_root: bool,
    pub is_ai_generated: bool,
    /// True when this manifest's signing certificate chains to the Charm Farm trust anchor.
    pub is_charmed: bool,
    /// Physical anchor hash from this manifest's org.trustsystems.physical-anchor assertion, if present.
    pub physical_hash: Option<String>,
    /// SHA256 fingerprint (hex) of the leaf cert that signed this manifest.
    pub signer_certificate_fingerprint: Option<String>,
    /// Full raw manifest as pretty-printed JSON, if extractable from reader.
    pub raw_manifest_json: Option<String>,
}

/// Ensures the output path is under a permitted base (home or temp) to prevent
/// arbitrary file writes to sensitive locations (e.g. /etc, .ssh).
fn ensure_output_path_safe(output: &str) -> Result<PathBuf> {
    let path = Path::new(output);
    let absolute = if path.is_absolute() {
        path.to_path_buf()
    } else {
        std::env::current_dir()
            .context("get current directory")?
            .join(path)
    };

    let parent = absolute
        .parent()
        .filter(|p| !p.as_os_str().is_empty())
        .unwrap_or(Path::new("."));
    let parent_canon = std::fs::canonicalize(parent)
        .with_context(|| format!("resolve output directory: {}", parent.display()))?;
    let resolved = parent_canon.join(
        absolute
            .file_name()
            .ok_or_else(|| anyhow::anyhow!("invalid output path: no filename"))?,
    );

    let home =
        dirs::home_dir().ok_or_else(|| anyhow::anyhow!("cannot determine home directory"))?;
    let home_canon = std::fs::canonicalize(&home)
        .with_context(|| format!("resolve home directory: {}", home.display()))?;
    let temp = std::env::temp_dir();
    let temp_canon = std::fs::canonicalize(&temp)
        .with_context(|| format!("resolve temp directory: {}", temp.display()))?;

    let under_home = resolved.starts_with(&home_canon);
    let under_temp = resolved.starts_with(&temp_canon);

    ensure!(
        under_home || under_temp,
        "output path must be under your home directory or system temp directory; got: {}",
        resolved.display()
    );
    Ok(resolved)
}

/// Normalizes the author string for use in a C2PA CreativeWork assertion.
/// Trims whitespace and rejects empty values.
pub fn format_author_for_assertion(author: &str) -> Result<String> {
    let trimmed = author.trim();
    ensure!(!trimmed.is_empty(), "author cannot be empty");
    Ok(trimmed.to_owned())
}

pub fn sign_image(input: &str, output: &str, cert: &str, key: &str, author: &str) -> Result<()> {
    let signer = SoftwareSigner::new(cert, key)?;
    sign_image_with_signer(input, output, "Trust Systems", author, CreativeWorkExtras::default(), None, &signer)
}

pub fn sign_image_with_manual_anchor(
    input: &str,
    output: &str,
    cert: &str,
    key: &str,
    author: &str,
    physical_hash: Option<&str>,
) -> Result<()> {
    let manual_anchor = match physical_hash {
        Some(hash) if !hash.trim().is_empty() => Some(ManualAnchor::new(hash)?),
        _ => None,
    };
    let signer = SoftwareSigner::new(cert, key)?;

    if let Some(anchor) = manual_anchor.as_ref() {
        sign_image_with_signer(
            input,
            output,
            "Trust Systems",
            author,
            CreativeWorkExtras::default(),
            Some(("manual", anchor)),
            &signer,
        )
    } else {
        sign_image_with_signer(input, output, "Trust Systems", author, CreativeWorkExtras::default(), None, &signer)
    }
}

pub fn sign_image_with_metadata(
    input: &str,
    output: &str,
    cert: &str,
    key: &str,
    title: &str,
    author: &str,
    physical_hash: Option<&str>,
) -> Result<()> {
    let manual_anchor = match physical_hash {
        Some(hash) if !hash.trim().is_empty() => Some(ManualAnchor::new(hash)?),
        _ => None,
    };
    let signer = SoftwareSigner::new(cert, key)?;
    if let Some(anchor) = manual_anchor.as_ref() {
        sign_image_with_signer(
            input,
            output,
            title,
            author,
            CreativeWorkExtras::default(),
            Some(("manual", anchor)),
            &signer,
        )
    } else {
        sign_image_with_signer(input, output, title, author, CreativeWorkExtras::default(), None, &signer)
    }
}

/// Anchor source for the manifest: manual hash string or NFC tag (uid + chip_family from vault).
pub enum PhysicalAnchorSource<'a> {
    Manual(&'a str),
    Nfc { uid: String, chip_family: String },
}

pub fn sign_image_with_materials(
    input: &str,
    output: &str,
    title: &str,
    author: &str,
    physical_hash: Option<&str>,
    cert_chain_pem: &[u8],
    private_key_pem: &[u8],
) -> Result<()> {
    let anchor = physical_hash
        .filter(|h| !h.trim().is_empty())
        .map(PhysicalAnchorSource::Manual);
    sign_image_with_materials_and_anchor(
        input,
        output,
        title,
        author,
        CreativeWorkExtras::default(),
        anchor,
        cert_chain_pem,
        private_key_pem,
    )
}

/// Optional CreativeWork metadata for manifest.
#[derive(Debug, Default, Clone)]
pub struct CreativeWorkExtras<'a> {
    pub description: Option<&'a str>,
    pub date_created: Option<&'a str>,
    pub license: Option<&'a str>,
    pub keywords: Option<&'a str>,
    pub genre: Option<&'a str>,
    pub material: Option<&'a str>,
    pub contributor: Option<&'a str>,
    pub abstract_: Option<&'a str>,
    pub ipfs_cid: Option<&'a str>,
    pub perceptual_hash: Option<&'a str>,
}

pub fn sign_image_with_materials_and_anchor(
    input: &str,
    output: &str,
    title: &str,
    author: &str,
    extras: CreativeWorkExtras<'_>,
    anchor: Option<PhysicalAnchorSource<'_>>,
    cert_chain_pem: &[u8],
    private_key_pem: &[u8],
) -> Result<()> {
    let signer = SoftwareSigner::from_pem_bytes(cert_chain_pem, private_key_pem)?;
    match anchor {
        Some(PhysicalAnchorSource::Manual(hash)) => {
            let manual = ManualAnchor::new(hash)?;
            sign_image_with_signer(
                input,
                output,
                title,
                author,
                extras,
                Some(("manual", &manual)),
                &signer,
            )
        }
        Some(PhysicalAnchorSource::Nfc { uid, chip_family }) => {
            sign_image_with_signer_nfc(
                input,
                output,
                title,
                author,
                extras,
                &uid,
                &chip_family,
                &signer,
            )
        }
        None => sign_image_with_signer(input, output, title, author, extras, None, &signer),
    }
}

pub fn sign_image_with_signer(
    input: &str,
    output: &str,
    title: &str,
    author: &str,
    extras: CreativeWorkExtras<'_>,
    anchor: Option<(&str, &dyn PhysicalAnchor)>,
    manifest_signer: &dyn ManifestSigner,
) -> Result<()> {
    let title_trimmed = title.trim();
    ensure!(!title_trimmed.is_empty(), "title cannot be empty");
    let author_str = format_author_for_assertion(author)?;
    ensure!(
        std::path::Path::new(input).exists(),
        "input file not found: {}",
        input
    );

    let output_resolved = ensure_output_path_safe(output)?;
    ensure!(
        !output_resolved.exists(),
        "output file already exists: {}",
        output_resolved.display()
    );

    let person = SchemaDotOrgPerson::new()
        .set_name(author_str.clone())
        .map_err(|e| anyhow::anyhow!("{}", e))?;
    let mut creative_work = CreativeWork::new()
        .add_author(person)
        .map_err(|e| anyhow::anyhow!("{}", e))?;
    creative_work = add_creative_work_extras(creative_work, &extras)?;

    let manifest_json = serde_json::json!({
        "title": title_trimmed,
        "claim_generator": CLAIM_GENERATOR
    })
    .to_string();
    let mut builder = Builder::from_json(&manifest_json).map_err(|e| anyhow::anyhow!("{}", e))?;
    builder.set_intent(BuilderIntent::Edit);
    builder
        .add_assertion(CreativeWork::LABEL, &creative_work)
        .map_err(|e| anyhow::anyhow!("{}", e))?;
    if let Some((anchor_type, anchor_impl)) = anchor {
        let assertion = PhysicalAnchorAssertion {
            physical_anchor_type: anchor_type.to_owned(),
            hash: anchor_impl.generate_handshake()?,
        };
        builder
            .add_assertion(PHYSICAL_ANCHOR_ASSERTION_LABEL, &assertion)
            .map_err(|e| anyhow::anyhow!("{}", e))?;
    }
    if let Some(phash) = extras.perceptual_hash.map(str::trim).filter(|s| !s.is_empty()) {
        let assertion = PerceptualHashAssertion {
            perceptual_hash: phash.to_string(),
        };
        builder
            .add_assertion(PERCEPTUAL_HASH_ASSERTION_LABEL, &assertion)
            .map_err(|e| anyhow::anyhow!("{}", e))?;
    }

    manifest_signer.sign(&mut builder, input, output_resolved.as_path())
}

fn add_creative_work_extras(
    mut cw: CreativeWork,
    extras: &CreativeWorkExtras<'_>,
) -> Result<CreativeWork> {
    if let Some(s) = extras.description.map(str::trim).filter(|s| !s.is_empty()) {
        cw = cw
            .insert("description", Value::String(s.to_string()))
            .map_err(|e| anyhow::anyhow!("{}", e))?;
    }
    if let Some(s) = extras.date_created.map(str::trim).filter(|s| !s.is_empty()) {
        cw = cw
            .insert("dateCreated", Value::String(s.to_string()))
            .map_err(|e| anyhow::anyhow!("{}", e))?;
    }
    if let Some(s) = extras.license.map(str::trim).filter(|s| !s.is_empty()) {
        cw = cw
            .insert("license", Value::String(s.to_string()))
            .map_err(|e| anyhow::anyhow!("{}", e))?;
    }
    if let Some(s) = extras.keywords.map(str::trim).filter(|s| !s.is_empty()) {
        cw = cw
            .insert("keywords", Value::String(s.to_string()))
            .map_err(|e| anyhow::anyhow!("{}", e))?;
    }
    if let Some(s) = extras.genre.map(str::trim).filter(|s| !s.is_empty()) {
        cw = cw
            .insert("genre", Value::String(s.to_string()))
            .map_err(|e| anyhow::anyhow!("{}", e))?;
    }
    if let Some(s) = extras.material.map(str::trim).filter(|s| !s.is_empty()) {
        cw = cw
            .insert("material", Value::String(s.to_string()))
            .map_err(|e| anyhow::anyhow!("{}", e))?;
    }
    if let Some(s) = extras.contributor.map(str::trim).filter(|s| !s.is_empty()) {
        let person = serde_json::json!({"@type": "Person", "name": s});
        cw = cw
            .insert("contributor", Value::Array(vec![person]))
            .map_err(|e| anyhow::anyhow!("{}", e))?;
    }
    if let Some(s) = extras.abstract_.map(str::trim).filter(|s| !s.is_empty()) {
        cw = cw
            .insert("abstract", Value::String(s.to_string()))
            .map_err(|e| anyhow::anyhow!("{}", e))?;
    }
    if let Some(cid) = extras.ipfs_cid.map(str::trim).filter(|s| !s.is_empty()) {
        cw = cw
            .insert("identifier", Value::String(format!("ipfs://{cid}")))
            .map_err(|e| anyhow::anyhow!("{}", e))?;
    }
    Ok(cw)
}

/// Extracts the content bytes used for C2PA content binding (and thus for IPFS CID).
/// For unsigned files: returns the full file bytes.
/// For signed files: returns file bytes excluding JUMBF ranges from the DataHash assertion.
pub fn extract_c2pa_content_bytes(path: &str) -> Result<Vec<u8>> {
    let path_buf = Path::new(path);
    ensure!(path_buf.exists(), "file not found: {}", path);
    let file_len = std::fs::metadata(path_buf)
        .with_context(|| format!("metadata for {}", path))?
        .len();

    let exclusions = try_get_data_hash_exclusions(path)?;
    let exclusions = match exclusions {
        Some(ex) if !ex.is_empty() => ex,
        _ => return std::fs::read(path_buf).with_context(|| format!("read file {}", path)),
    };

    // Build included ranges: [0, file_len) minus exclusion ranges.
    // Exclusions are (start, length); we exclude [start, start+length).
    let mut ranges: Vec<(u64, u64)> = exclusions
        .iter()
        .map(|r| (r.start(), r.length()))
        .collect();
    ranges.sort_by_key(|(s, _)| *s);

    let mut out = Vec::new();
    let mut pos: u64 = 0;
    for (start, len) in ranges {
        let end = start.saturating_add(len).min(file_len);
        if start > pos {
            let to_read = (start - pos) as usize;
            let mut buf = vec![0u8; to_read];
            let mut f = std::fs::File::open(path_buf)
                .with_context(|| format!("open {}", path))?;
            f.seek(SeekFrom::Start(pos))?;
            f.read_exact(&mut buf)?;
            out.extend_from_slice(&buf);
        }
        pos = end.max(pos);
    }
    if pos < file_len {
        let to_read = (file_len - pos) as usize;
        let mut buf = vec![0u8; to_read];
        let mut f = std::fs::File::open(path_buf)
            .with_context(|| format!("open {}", path))?;
        f.seek(SeekFrom::Start(pos))?;
        f.read_exact(&mut buf)?;
        out.extend_from_slice(&buf);
    }
    Ok(out)
}

/// Computes IPFS CID from content bytes using CIDv1 + raw-leaves (256KB chunks, balanced tree).
/// Returns CIDv1 Base32 string (e.g. bafybe...). Matches Kubo `ipfs add --cid-version=1`.
pub fn compute_ipfs_cid_from_bytes(bytes: &[u8]) -> Result<String> {
    ipfs::compute_ipfs_cid_from_bytes(bytes)
}

fn try_get_data_hash_exclusions(path: &str) -> Result<Option<Vec<c2pa::HashRange>>> {
    let settings = Settings::new()
        .with_json(r#"{"verify": {"verify_trust": false}}"#)
        .map_err(|e| anyhow::anyhow!("{}", e))?;
    let reader = match Reader::from_context(
        c2pa::Context::new()
            .with_settings(settings)
            .map_err(|e| anyhow::anyhow!("{}", e))?,
    )
    .with_file(path)
    {
        Ok(r) => r,
        Err(_) => return Ok(None),
    };
    let manifest = match reader.active_manifest() {
        Some(m) => m,
        None => return Ok(None),
    };
    for assertion in manifest.assertions() {
        if assertion.label() != C2PA_HASH_DATA_LABEL {
            continue;
        }
        let data_hash = match assertion.to_assertion::<DataHash>() {
            Ok(dh) => dh,
            Err(_) => continue,
        };
        return Ok(data_hash.exclusions);
    }
    Ok(None)
}

fn sign_image_with_signer_nfc(
    input: &str,
    output: &str,
    title: &str,
    author: &str,
    extras: CreativeWorkExtras<'_>,
    uid: &str,
    chip_family: &str,
    manifest_signer: &dyn ManifestSigner,
) -> Result<()> {
    let title_trimmed = title.trim();
    ensure!(!title_trimmed.is_empty(), "title cannot be empty");
    let author_str = format_author_for_assertion(author)?;
    ensure!(
        std::path::Path::new(input).exists(),
        "input file not found: {}",
        input
    );

    let output_resolved = ensure_output_path_safe(output)?;
    ensure!(
        !output_resolved.exists(),
        "output file already exists: {}",
        output_resolved.display()
    );

    let person = SchemaDotOrgPerson::new()
        .set_name(author_str.clone())
        .map_err(|e| anyhow::anyhow!("{}", e))?;
    let mut creative_work = CreativeWork::new()
        .add_author(person)
        .map_err(|e| anyhow::anyhow!("{}", e))?;
    creative_work = add_creative_work_extras(creative_work, &extras)?;

    let manifest_json = serde_json::json!({
        "title": title_trimmed,
        "claim_generator": CLAIM_GENERATOR
    })
    .to_string();
    let mut builder = Builder::from_json(&manifest_json).map_err(|e| anyhow::anyhow!("{}", e))?;
    builder.set_intent(BuilderIntent::Edit);
    builder
        .add_assertion(CreativeWork::LABEL, &creative_work)
        .map_err(|e| anyhow::anyhow!("{}", e))?;

    let chip_family_display = format_chip_family_display(chip_family);
    let assertion = PhysicalAnchorAssertionNfc {
        physical_anchor_type: "nfc".to_owned(),
        chip_family: chip_family_display,
        uid: uid.trim().to_uppercase(),
    };
    builder
        .add_assertion(PHYSICAL_ANCHOR_ASSERTION_LABEL, &assertion)
        .map_err(|e| anyhow::anyhow!("{}", e))?;
    if let Some(phash) = extras.perceptual_hash.map(str::trim).filter(|s| !s.is_empty()) {
        let phash_assertion = PerceptualHashAssertion {
            perceptual_hash: phash.to_string(),
        };
        builder
            .add_assertion(PERCEPTUAL_HASH_ASSERTION_LABEL, &phash_assertion)
            .map_err(|e| anyhow::anyhow!("{}", e))?;
    }

    manifest_signer.sign(&mut builder, input, output_resolved.as_path())
}

/// Transfer ownership by appending a new manifest that records publication to a recipient.
/// Uses BuilderIntent::Edit for manifest chaining and attempts to carry over dynamic physical
/// anchor assertions discovered in the active manifest.
/// If `recipient_cert_fingerprint` is provided (SHA256 hex of recipient cert), it is stored in
/// SchemaDotOrgPerson.identifier for trusted-contact verification in Inspector.
pub fn transfer_asset_with_materials(
    input: &str,
    output: &str,
    recipient_name: &str,
    cert_chain_pem: &[u8],
    private_key_pem: &[u8],
    recipient_cert_fingerprint: Option<&str>,
) -> Result<()> {
    let signer = SoftwareSigner::from_pem_bytes(cert_chain_pem, private_key_pem)?;
    transfer_asset_with_signer(input, output, recipient_name, recipient_cert_fingerprint, &signer)
}

fn transfer_asset_with_signer(
    input: &str,
    output: &str,
    recipient_name: &str,
    recipient_cert_fingerprint: Option<&str>,
    manifest_signer: &dyn ManifestSigner,
) -> Result<()> {
    ensure!(
        std::path::Path::new(input).exists(),
        "input file not found: {}",
        input
    );
    let recipient = format_author_for_assertion(recipient_name)
        .map_err(|_| anyhow::anyhow!("recipient name cannot be empty"))?;

    let output_resolved = ensure_output_path_safe(output)?;
    ensure!(
        !output_resolved.exists(),
        "output file already exists: {}",
        output_resolved.display()
    );

    let settings = Settings::new()
        .with_json(r#"{"verify": {"verify_trust": false}}"#)
        .map_err(|e| anyhow::anyhow!("{}", e))?;
    let reader = Reader::from_context(
        c2pa::Context::new()
            .with_settings(settings)
            .map_err(|e| anyhow::anyhow!("{}", e))?,
    )
    .with_file(input)
    .map_err(|e| anyhow::anyhow!("{}", e))
    .context("read C2PA manifest from file for transfer")?;
    let active = reader
        .active_manifest()
        .ok_or_else(|| anyhow::anyhow!("no active manifest found for transfer"))?;

    let title = active.title().unwrap_or("Transferred Asset");
    let manifest_json = serde_json::json!({
        "title": title,
        "claim_generator": CLAIM_GENERATOR
    })
    .to_string();
    let mut builder = Builder::from_json(&manifest_json).map_err(|e| anyhow::anyhow!("{}", e))?;
    builder.set_intent(BuilderIntent::Edit);

    let actions = Actions::new().add_action(Action::new(c2pa_action::PUBLISHED));
    builder
        .add_assertion(Actions::LABEL, &actions)
        .map_err(|e| anyhow::anyhow!("{}", e))?;

    let origin = origin_manifest_for_active(&reader, Some(active));
    let source_cw = origin
        .and_then(|m| m.find_assertion::<CreativeWork>(CreativeWork::LABEL).ok())
        .or_else(|| active.find_assertion::<CreativeWork>(CreativeWork::LABEL).ok());
    let creative_work = creative_work_from_source_with_publisher(
        source_cw.as_ref(),
        recipient,
        recipient_cert_fingerprint,
    )?;
    builder
        .add_assertion(CreativeWork::LABEL, &creative_work)
        .map_err(|e| anyhow::anyhow!("{}", e))?;

    if let Some((label, value)) = find_dynamic_anchor_assertion(active.assertions()) {
        builder
            .add_assertion(&label, &value)
            .map_err(|e| anyhow::anyhow!("{}", e))?;
    }

    manifest_signer.sign(&mut builder, input, output_resolved.as_path())
}

/// Builds a CreativeWork for transfer by copying metadata from the source manifest's
/// CreativeWork and adding the publisher (recipient). Preserves author, description,
/// dateCreated, license, keywords, genre, material, contributor, abstract, identifier.
fn creative_work_from_source_with_publisher(
    source: Option<&CreativeWork>,
    recipient: String,
    recipient_cert_fingerprint: Option<&str>,
) -> Result<CreativeWork> {
    let mut publisher_person = SchemaDotOrgPerson::new()
        .set_name(recipient)
        .map_err(|e| anyhow::anyhow!("{}", e))?;
    if let Some(fp) = recipient_cert_fingerprint.filter(|s| !s.trim().is_empty()) {
        publisher_person = publisher_person
            .set_identifier(fp)
            .map_err(|e| anyhow::anyhow!("{}", e))?;
    }
    let mut cw = CreativeWork::new()
        .insert("publisher", publisher_person)
        .map_err(|e| anyhow::anyhow!("{}", e))?;

    let Some(src) = source else {
        return Ok(cw);
    };

    if let Some(persons) = src.author() {
        if let Some(first) = persons.first() {
            if let Some(name) = first.name() {
                let person = SchemaDotOrgPerson::new()
                    .set_name(name.to_string())
                    .map_err(|e| anyhow::anyhow!("{}", e))?;
                cw = cw.add_author(person).map_err(|e| anyhow::anyhow!("{}", e))?;
            }
        }
    }
    if let Some(v) = src.get::<String>("description").filter(|s| !s.trim().is_empty()) {
        cw = cw.insert("description", Value::String(v)).map_err(|e| anyhow::anyhow!("{}", e))?;
    }
    if let Some(v) = src.get::<String>("dateCreated").filter(|s| !s.trim().is_empty()) {
        cw = cw.insert("dateCreated", Value::String(v)).map_err(|e| anyhow::anyhow!("{}", e))?;
    }
    if let Some(v) = src.get::<String>("license").filter(|s| !s.trim().is_empty()) {
        cw = cw.insert("license", Value::String(v)).map_err(|e| anyhow::anyhow!("{}", e))?;
    }
    if let Some(v) = src.get::<String>("keywords").filter(|s| !s.trim().is_empty()) {
        cw = cw.insert("keywords", Value::String(v)).map_err(|e| anyhow::anyhow!("{}", e))?;
    }
    if let Some(v) = src.get::<String>("genre").filter(|s| !s.trim().is_empty()) {
        cw = cw.insert("genre", Value::String(v)).map_err(|e| anyhow::anyhow!("{}", e))?;
    }
    if let Some(v) = src.get::<String>("material").filter(|s| !s.trim().is_empty()) {
        cw = cw.insert("material", Value::String(v)).map_err(|e| anyhow::anyhow!("{}", e))?;
    }
    if let Some(v) = src.get::<String>("abstract").filter(|s| !s.trim().is_empty()) {
        cw = cw.insert("abstract", Value::String(v)).map_err(|e| anyhow::anyhow!("{}", e))?;
    }
    if let Some(v) = src.get::<String>("identifier").filter(|s| !s.trim().is_empty()) {
        cw = cw.insert("identifier", Value::String(v)).map_err(|e| anyhow::anyhow!("{}", e))?;
    }
    if let Some(contributors) = src.get::<Vec<SchemaDotOrgPerson>>("contributor") {
        if let Some(first) = contributors.first().and_then(|p| p.name()) {
            let person = serde_json::json!({"@type": "Person", "name": first});
            cw = cw
                .insert("contributor", Value::Array(vec![person]))
                .map_err(|e| anyhow::anyhow!("{}", e))?;
        }
    }
    Ok(cw)
}

fn find_dynamic_anchor_assertion(
    assertions: &[c2pa::ManifestAssertion],
) -> Option<(String, serde_json::Value)> {
    for assertion in assertions {
        let value = match assertion.value() {
            Ok(v) => v,
            Err(_) => continue,
        };
        if json_contains_anchor_keys(value) {
            return Some((assertion.label().to_string(), value.clone()));
        }
    }
    None
}

fn json_contains_anchor_keys(value: &Value) -> bool {
    match value {
        Value::Object(map) => {
            if map.contains_key("tag_uid") || map.contains_key("sun_cmac") {
                return true;
            }
            map.values().any(json_contains_anchor_keys)
        }
        Value::Array(items) => items.iter().any(json_contains_anchor_keys),
        _ => false,
    }
}

fn signer_leaf_fingerprint_sha256(manifest: Option<&c2pa::Manifest>) -> Option<String> {
    let cert_chain_pem = manifest
        .and_then(|m| m.signature_info())
        .map(|s| s.cert_chain())?;
    let mut reader = BufReader::new(cert_chain_pem.as_bytes());
    let chain = certs(&mut reader).collect::<std::result::Result<Vec<_>, _>>().ok()?;
    let leaf = chain.first()?;
    let mut hasher = Sha256::new();
    hasher.update(leaf);
    Some(hex::encode(hasher.finalize()))
}

fn parse_cert_chain_ders(pem_bundle: &str) -> Vec<Vec<u8>> {
    let mut reader = BufReader::new(pem_bundle.as_bytes());
    certs(&mut reader)
        .collect::<std::result::Result<Vec<_>, _>>()
        .unwrap_or_default()
        .into_iter()
        .map(|c| c.to_vec())
        .collect()
}

fn cert_fingerprint_sha256_der(cert_der: &[u8]) -> String {
    let mut hasher = Sha256::new();
    hasher.update(cert_der);
    hex::encode(hasher.finalize())
}

/// If the cert chain PEM doesn't already contain the built-in Charm Farm root,
/// append it. Returns the (possibly augmented) chain as bytes suitable for signing.
pub fn ensure_chain_includes_trust_anchor(chain_pem: &[u8]) -> Vec<u8> {
    let chain_str = match std::str::from_utf8(chain_pem) {
        Ok(s) => s,
        Err(_) => return chain_pem.to_vec(),
    };
    let chain_ders = parse_cert_chain_ders(chain_str);
    let anchor_fingerprints: HashSet<String> = parse_cert_chain_ders(ORG_TRUST_ANCHORS_PEM)
        .iter()
        .map(|der| cert_fingerprint_sha256_der(der))
        .collect();
    if anchor_fingerprints.is_empty() {
        return chain_pem.to_vec();
    }
    let already_present = chain_ders
        .iter()
        .map(|der| cert_fingerprint_sha256_der(der))
        .any(|fp| anchor_fingerprints.contains(&fp));
    if already_present {
        return chain_pem.to_vec();
    }
    let mut augmented = chain_pem.to_vec();
    if !augmented.ends_with(b"\n") {
        augmented.push(b'\n');
    }
    augmented.extend_from_slice(ORG_TRUST_ANCHORS_PEM.as_bytes());
    augmented
}

/// Determines whether a manifest's signing certificate chain includes the built-in Charm Farm
/// trust anchor. Uses SHA-256 fingerprint comparison of DER-encoded certificates.
fn manifest_is_charmed(manifest: &c2pa::Manifest) -> bool {
    let Some(sig) = manifest.signature_info() else {
        return false;
    };
    let chain = parse_cert_chain_ders(sig.cert_chain());
    if chain.is_empty() {
        return false;
    }
    let anchor_fingerprints: HashSet<String> = parse_cert_chain_ders(ORG_TRUST_ANCHORS_PEM)
        .iter()
        .map(|der| cert_fingerprint_sha256_der(der))
        .collect();
    if anchor_fingerprints.is_empty() {
        return false;
    }
    chain
        .iter()
        .map(|der| cert_fingerprint_sha256_der(der))
        .any(|fp| anchor_fingerprints.contains(&fp))
}

fn active_manifest_is_charmed(manifest: Option<&c2pa::Manifest>) -> bool {
    manifest.map_or(false, manifest_is_charmed)
}

/// Fallback: scan all CreativeWork assertions for identifier (handles multiple or differently-keyed).
fn extract_ipfs_from_creative_work_assertions(manifest: Option<&c2pa::Manifest>) -> Option<String> {
    let manifest = manifest?;
    let base_label = "stds.schema-org.CreativeWork";
    for assertion in manifest.assertions() {
        let label = assertion.label();
        if !label.starts_with(base_label) {
            continue;
        }
        if let Ok(cw) = assertion.to_assertion::<CreativeWork>() {
            if let Some(id) = cw.get::<String>("identifier") {
                if id.starts_with("ipfs://") {
                    return Some(id);
                }
            }
        }
    }
    None
}

/// Fallback: extract identifier from raw manifest JSON (handles schema.org variations).
fn extract_ipfs_from_manifest_json(
    manifest: Option<&c2pa::Manifest>,
    manifests_map: &Option<std::collections::HashMap<String, Value>>,
) -> Option<String> {
    let manifest = manifest?;
    let map = manifests_map.as_ref()?;
    let manifest_label = manifest.label()?;
    let manifest_data = map.get(manifest_label)?;
    let assertions = manifest_data.get("assertions")?.as_array()?;
    let base_label = "stds.schema-org.CreativeWork";
    for assertion in assertions {
        let assertion_obj = assertion.as_object()?;
        let label = assertion_obj.get("label")?.as_str()?;
        if !label.starts_with(base_label) {
            continue;
        }
        let data = assertion_obj.get("data").or_else(|| assertion_obj.get("data_"))?;
        if let Some(id) = data.get("identifier") {
            if let Some(s) = id.as_str() {
                if s.starts_with("ipfs://") {
                    return Some(s.to_string());
                }
            }
            if let Some(arr) = id.as_array() {
                for item in arr {
                    if let Some(s) = item.as_str() {
                        if s.starts_with("ipfs://") {
                            return Some(s.to_string());
                        }
                    }
                }
            }
        }
    }
    None
}

fn manifest_author(manifest: &c2pa::Manifest) -> Option<String> {
    manifest
        .find_assertion::<CreativeWork>(CreativeWork::LABEL)
        .ok()
        .and_then(|cw| cw.author())
        .and_then(|persons| persons.first().and_then(|p| p.name()))
        .map(String::from)
}

fn manifest_publisher(manifest: &c2pa::Manifest) -> Option<String> {
    manifest
        .find_assertion::<CreativeWork>(CreativeWork::LABEL)
        .ok()
        .and_then(|cw| cw.get::<SchemaDotOrgPerson>("publisher"))
        .and_then(|p| p.name())
}

fn manifest_actions(manifest: &c2pa::Manifest) -> Vec<String> {
    manifest
        .find_assertion::<Actions>(Actions::LABEL)
        .ok()
        .map(|actions| {
            actions
                .actions()
                .iter()
                .map(|a| a.action().to_string())
                .collect::<Vec<_>>()
        })
        .unwrap_or_default()
}

fn format_chip_family_display(raw: &str) -> String {
    let s = raw.trim();
    if s.eq_ignore_ascii_case("NTAG424DNA") || s.eq_ignore_ascii_case("NTAG424") {
        "NTAG 424 DNA".to_string()
    } else if !s.is_empty() {
        s.to_string()
    } else {
        "NTAG 424 DNA".to_string()
    }
}

fn manifest_perceptual_hash(manifest: &c2pa::Manifest) -> Option<String> {
    let a = manifest
        .find_assertion::<PerceptualHashAssertion>(PERCEPTUAL_HASH_ASSERTION_LABEL)
        .ok()?;
    let h = a.perceptual_hash.trim();
    if h.is_empty() {
        None
    } else {
        Some(h.to_string())
    }
}

fn manifest_physical_hash(manifest: &c2pa::Manifest) -> Option<String> {
    let a = manifest
        .find_assertion::<PhysicalAnchorAssertionRead>(PHYSICAL_ANCHOR_ASSERTION_LABEL)
        .ok()
        .or_else(|| {
            manifest
                .find_assertion::<PhysicalAnchorAssertionRead>(PHYSICAL_ANCHOR_ASSERTION_LABEL_LEGACY)
                .ok()
        })?;
    // Legacy: hash field. NFC: derive nfc:uid:XXX for correlation.
    a.hash
        .as_ref()
        .filter(|h| !h.is_empty())
        .cloned()
        .or_else(|| a.uid.as_ref().map(|u| format!("nfc:uid:{}", u.to_uppercase())))
}

fn origin_manifest_for_active<'a>(
    reader: &'a Reader,
    active: Option<&'a c2pa::Manifest>,
) -> Option<&'a c2pa::Manifest> {
    let active = active?;
    let manifests: Vec<&c2pa::Manifest> = reader.iter_manifests().collect();
    if manifests.is_empty() {
        return Some(active);
    }
    let by_label: HashMap<&str, &c2pa::Manifest> = manifests
        .iter()
        .filter_map(|m| m.label().map(|label| (label, *m)))
        .collect();
    let by_instance_id: HashMap<&str, &c2pa::Manifest> = manifests
        .iter()
        .filter_map(|m| {
            let id = m.instance_id();
            if id.is_empty() {
                None
            } else {
                Some((id, *m))
            }
        })
        .collect();

    let mut current = active;
    for _ in 0..=manifests.len() {
        let Some(parent) = current.parent() else {
            break;
        };
        let parent_label = parent.active_manifest();
        let parent_instance_id = parent.instance_id();
        let next = parent_label
            .and_then(|label| by_label.get(label).copied().or_else(|| {
                manifests
                    .iter()
                    .find(|m| m.label().map(|l| label.ends_with(l)).unwrap_or(false))
                    .copied()
            }))
            .or_else(|| {
                if parent_instance_id.is_empty() || parent_instance_id == "None" {
                    None
                } else {
                    by_instance_id.get(parent_instance_id).copied()
                }
            });
        let Some(next) = next else {
            break;
        };
        if std::ptr::eq(current, next) {
            break;
        }
        current = next;
    }
    Some(current)
}

fn manifest_chain_for_active<'a>(
    reader: &'a Reader,
    active: Option<&'a c2pa::Manifest>,
) -> Vec<&'a c2pa::Manifest> {
    let Some(active) = active else {
        return Vec::new();
    };
    let manifests: Vec<&c2pa::Manifest> = reader.iter_manifests().collect();
    if manifests.is_empty() {
        return vec![active];
    }
    let by_label: HashMap<&str, &c2pa::Manifest> = manifests
        .iter()
        .filter_map(|m| m.label().map(|label| (label, *m)))
        .collect();
    let by_instance_id: HashMap<&str, &c2pa::Manifest> = manifests
        .iter()
        .filter_map(|m| {
            let id = m.instance_id();
            if id.is_empty() {
                None
            } else {
                Some((id, *m))
            }
        })
        .collect();

    let mut chain = vec![active];
    let mut current = active;
    for _ in 0..=manifests.len() {
        let Some(parent) = current.parent() else {
            break;
        };
        let parent_label = parent.active_manifest();
        let parent_instance_id = parent.instance_id();
        let next = parent_label
            .and_then(|label| by_label.get(label).copied().or_else(|| {
                manifests
                    .iter()
                    .find(|m| m.label().map(|l| label.ends_with(l)).unwrap_or(false))
                    .copied()
            }))
            .or_else(|| {
                if parent_instance_id.is_empty() || parent_instance_id == "None" {
                    None
                } else {
                    by_instance_id.get(parent_instance_id).copied()
                }
            });
        let Some(next) = next else {
            break;
        };
        if chain.iter().any(|m| std::ptr::eq(*m, next)) {
            break;
        }
        chain.push(next);
        current = next;
    }
    chain.reverse();
    chain
}

/// Returns raw manifest store JSON for debugging (e.g. to inspect CreativeWork structure).
pub fn get_manifest_json(input: &str) -> Result<String> {
    ensure!(
        std::path::Path::new(input).exists(),
        "input file not found: {}",
        input
    );
    let settings = Settings::new()
        .with_json(r#"{"verify": {"verify_trust": false}}"#)
        .map_err(|e| anyhow::anyhow!("{}", e))?;
    let reader = Reader::from_context(
        c2pa::Context::new()
            .with_settings(settings)
            .map_err(|e| anyhow::anyhow!("{}", e))?,
    )
    .with_file(input)
    .map_err(|e| anyhow::anyhow!("{}", e))
    .context("read C2PA manifest from file")?;
    Ok(reader.json())
}

pub fn verify_image(input: &str) -> Result<VerificationResult> {
    ensure!(
        std::path::Path::new(input).exists(),
        "input file not found: {}",
        input
    );

    let settings = Settings::new()
        .with_json(r#"{"verify": {"verify_trust": false}}"#)
        .map_err(|e| anyhow::anyhow!("{}", e))?;
    let reader = Reader::from_context(
        c2pa::Context::new()
            .with_settings(settings)
            .map_err(|e| anyhow::anyhow!("{}", e))?,
    )
    .with_file(input)
    .map_err(|e| anyhow::anyhow!("{}", e))
    .context("read C2PA manifest from file")?;

    let state = reader.validation_state();

    let status = match state {
        ValidationState::Trusted | ValidationState::Valid => ValidationStatus::Success,
        ValidationState::Invalid => ValidationStatus::Failure,
    };

    let manifest = reader.active_manifest();
    let chain = manifest_chain_for_active(&reader, manifest);
    let origin_manifest = origin_manifest_for_active(&reader, manifest);

    let full_json = reader.json();
    let manifests_map: Option<std::collections::HashMap<String, Value>> =
        serde_json::from_str::<Value>(&full_json)
            .ok()
            .and_then(|root| {
                root.get("manifests")
                    .and_then(Value::as_object)
                    .map(|obj| obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
            });
    let is_charmed = active_manifest_is_charmed(manifest);
    let signer_certificate_fingerprint = signer_leaf_fingerprint_sha256(manifest);
    let author = manifest.and_then(manifest_author);
    let title = manifest.and_then(|m| m.title()).map(String::from);
    let claim_generator = manifest.and_then(|m| m.claim_generator()).map(String::from);
    let signed_at = manifest.and_then(|m| m.time()).map(String::from);
    let signer = manifest.and_then(|m| m.common_name().or_else(|| m.issuer()));
    let format = manifest.and_then(|m| m.format()).map(String::from);
    let instance_id = manifest.map(|m| m.instance_id().to_string());
    let assertion_labels = manifest
        .map(|m| {
            m.assertions()
                .iter()
                .map(|a| a.label().to_string())
                .collect()
        })
        .unwrap_or_default();
    let physical_hash = manifest.and_then(manifest_physical_hash);
    let perceptual_hash = manifest.and_then(manifest_perceptual_hash);
    let publisher = manifest
        .and_then(|m| m.find_assertion::<CreativeWork>(CreativeWork::LABEL).ok())
        .and_then(|cw| cw.get::<SchemaDotOrgPerson>("publisher"))
        .and_then(|p| p.name());
    let publisher_identifier = manifest
        .and_then(|m| m.find_assertion::<CreativeWork>(CreativeWork::LABEL).ok())
        .and_then(|cw| cw.get::<SchemaDotOrgPerson>("publisher"))
        .and_then(|p| p.identifier());
    let creative_work = manifest.and_then(|m| m.find_assertion::<CreativeWork>(CreativeWork::LABEL).ok());
    let description = creative_work.as_ref().and_then(|cw| cw.get::<String>("description"));
    let date_created = creative_work.as_ref().and_then(|cw| cw.get::<String>("dateCreated"));
    let license = creative_work.as_ref().and_then(|cw| cw.get::<String>("license"));
    let keywords = creative_work.as_ref().and_then(|cw| cw.get::<String>("keywords"));
    let genre = creative_work.as_ref().and_then(|cw| cw.get::<String>("genre"));
    let material = creative_work.as_ref().and_then(|cw| cw.get::<String>("material"));
    let contributor = creative_work
        .as_ref()
        .and_then(|cw| cw.get::<Vec<SchemaDotOrgPerson>>("contributor"))
        .and_then(|v| v.first().cloned())
        .and_then(|p| p.name());
    let abstract_ = creative_work.as_ref().and_then(|cw| cw.get::<String>("abstract"));
    let ipfs_cid = creative_work
        .as_ref()
        .and_then(|cw| cw.get::<String>("identifier"))
        .filter(|s| s.starts_with("ipfs://"))
        .or_else(|| extract_ipfs_from_creative_work_assertions(manifest))
        .or_else(|| extract_ipfs_from_manifest_json(manifest, &manifests_map));
    let origin_author = origin_manifest.and_then(manifest_author);
    let origin_title = origin_manifest.and_then(|m| m.title()).map(String::from);
    let origin_signed_at = origin_manifest.and_then(|m| m.time()).map(String::from);
    let active_label = manifest.and_then(|m| m.label().map(str::to_string));
    let manifest_history = chain
        .iter()
        .enumerate()
        .map(|(idx, m)| {
            let assertion_labels = m
                .assertions()
                .iter()
                .map(|a| a.label().to_string())
                .collect::<Vec<_>>();
            let is_ai_generated = assertion_labels.iter().any(|l| {
                let lower = l.to_lowercase();
                lower.contains("ai_generated_content")
                    || lower.contains("trainedalgorithmicmedia")
                    || lower.contains("c2pa.ai.generated")
                    || lower.contains("ai.generated")
            });
            ManifestHistoryEntry {
                label: m.label().map(str::to_string),
                instance_id: Some(m.instance_id().to_string()),
                title: m.title().map(String::from),
                author: manifest_author(m),
                publisher: manifest_publisher(m),
                claim_generator: m.claim_generator().map(String::from),
                signer: m.common_name().or_else(|| m.issuer()),
                signed_at: m.time(),
                actions: manifest_actions(m),
                assertion_labels,
                is_active: active_label
                    .as_deref()
                    .zip(m.label())
                    .map(|(a, b)| a == b)
                    .unwrap_or(false),
                is_root: idx == 0,
                is_ai_generated,
                is_charmed: manifest_is_charmed(m),
                physical_hash: manifest_physical_hash(m),
                signer_certificate_fingerprint: signer_leaf_fingerprint_sha256(Some(m)),
                raw_manifest_json: m.label()
                    .and_then(|label| manifests_map.as_ref().and_then(|map| map.get(label)))
                    .and_then(|v| serde_json::to_string_pretty(v).ok()),
            }
        })
        .collect::<Vec<_>>();

    let status_explanation = if is_charmed {
        "Manifest is valid and the signing certificate chains to the Charm Farm trust anchor."
            .to_string()
    } else {
        match status {
            ValidationStatus::Success => {
                "Manifest has a valid cryptographic signature.".to_string()
            }
            ValidationStatus::Untrusted => {
                "Manifest has a valid cryptographic signature.".to_string()
            }
            ValidationStatus::Failure => {
                "Manifest validation failed (invalid or tampered).".to_string()
            }
        }
    };

    Ok(VerificationResult {
        status,
        status_explanation,
        author,
        title,
        claim_generator,
        signed_at,
        signer,
        format,
        instance_id,
        assertion_labels,
        physical_hash,
        publisher,
        publisher_identifier,
        signer_certificate_fingerprint,
        origin_author,
        origin_title,
        origin_signed_at,
        description,
        date_created,
        license,
        keywords,
        genre,
        material,
        contributor,
        abstract_,
        ipfs_cid,
        perceptual_hash,
        is_charmed,
        manifest_history,
    })
}

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

    use super::*;
    use c2pa::ManifestAssertion;
    use rustls_pemfile::certs;
    use sha2::{Digest, Sha256};

    #[test]
    fn format_author_trimmed_and_non_empty() {
        assert_eq!(
            format_author_for_assertion("  Jane Doe  ").unwrap(),
            "Jane Doe"
        );
        assert_eq!(format_author_for_assertion("Jane Doe").unwrap(), "Jane Doe");
    }

    #[test]
    fn format_author_rejects_empty() {
        assert!(format_author_for_assertion("").is_err());
        assert!(format_author_for_assertion("   ").is_err());
    }

    #[test]
    fn validation_status_display() {
        assert_eq!(ValidationStatus::Success.to_string(), "Success");
        assert_eq!(ValidationStatus::Failure.to_string(), "Failure");
        assert_eq!(ValidationStatus::Untrusted.to_string(), "Untrusted");
    }

    #[test]
    fn verify_image_fails_when_manifest_missing() {
        let path = env!("CARGO_MANIFEST_DIR").to_string() + "/assets/test_data/unsigned.jpg";
        if !std::path::Path::new(&path).exists() {
            return;
        }
        let err = verify_image(&path).unwrap_err();
        let msg = err.to_string();
        assert!(msg.contains("JUMBF") || msg.contains("manifest") || msg.contains("read"));
    }

    #[test]
    fn verify_image_fails_for_missing_file() {
        let err = verify_image("/nonexistent/path.jpg").unwrap_err();
        assert!(err.to_string().contains("not found"));
    }

    #[test]
    fn verify_image_success_returns_status_and_author() {
        let base = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("assets")
            .join("test_data");
        let source = base.join("sample.jpg");
        let cert = base.join("es256_certs.pem");
        let key = base.join("es256_private.key");
        if !source.exists() || !cert.exists() || !key.exists() {
            return;
        }
        let out = std::env::temp_dir().join(format!(
            "trust_systems_verify_success_test_{}.jpg",
            std::process::id()
        ));
        let _ = std::fs::remove_file(&out);

        sign_image(
            source.to_str().unwrap(),
            out.to_str().unwrap(),
            cert.to_str().unwrap(),
            key.to_str().unwrap(),
            "Verify Success Author",
        )
        .unwrap();

        let result = verify_image(out.to_str().unwrap()).unwrap();
        assert_eq!(result.status, ValidationStatus::Success);
        assert_eq!(result.author.as_deref(), Some("Verify Success Author"));
        assert!(
            !result.is_charmed,
            "CAI test certs should not be charmed (not signed by Charm Farm root)"
        );

        assert!(!result.status_explanation.is_empty());
        assert!(result
            .assertion_labels
            .contains(&CreativeWork::LABEL.to_string()));
        assert_eq!(result.title.as_deref(), Some("Trust Systems"));
        assert!(result.instance_id.is_some());

        let _ = std::fs::remove_file(&out);
    }

    #[test]
    fn manual_anchor_rejects_empty_hash() {
        assert!(ManualAnchor::new("").is_err());
        assert!(ManualAnchor::new("   ").is_err());
    }

    #[test]
    fn sign_image_with_manual_anchor_includes_custom_assertion_label() {
        let base = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("assets")
            .join("test_data");
        let source = base.join("sample.jpg");
        let cert = base.join("es256_certs.pem");
        let key = base.join("es256_private.key");
        if !source.exists() || !cert.exists() || !key.exists() {
            return;
        }

        let out = std::env::temp_dir().join(format!(
            "trust_systems_anchor_sign_test_{}.jpg",
            std::process::id()
        ));
        let _ = std::fs::remove_file(&out);

        sign_image_with_manual_anchor(
            source.to_str().unwrap(),
            out.to_str().unwrap(),
            cert.to_str().unwrap(),
            key.to_str().unwrap(),
            "Anchor Author",
            Some("manual-hash-123"),
        )
        .unwrap();

        let result = verify_image(out.to_str().unwrap()).unwrap();
        assert!(
            result
                .assertion_labels
                .iter()
                .any(|label| label.contains("physical-anchor")),
            "expected assertion labels to include physical-anchor, got: {:?}",
            result.assertion_labels
        );

        let _ = std::fs::remove_file(&out);
    }

    #[test]
    fn signed_with_handshake_fixture_exposes_physical_hash() {
        let fixture = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("assets")
            .join("test_data")
            .join("signed_with_handshake.jpg");
        if !fixture.exists() {
            return;
        }
        let result = verify_image(fixture.to_str().unwrap()).expect("verify fixture");
        assert!(
            result.physical_hash.is_some(),
            "expected physical_hash from fixture, got: {:?}",
            result.assertion_labels
        );
    }

    #[test]
    fn sign_with_ipfs_cid_roundtrips_in_verification() {
        let base = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("assets")
            .join("test_data");
        let source = base.join("unsigned.jpg");
        let cert = base.join("es256_certs.pem");
        let key = base.join("es256_private.key");
        if !source.exists() || !cert.exists() || !key.exists() {
            return;
        }

        let out = std::env::temp_dir().join(format!(
            "trust_systems_ipfs_cid_test_{}.jpg",
            std::process::id()
        ));
        let _ = std::fs::remove_file(&out);

        let cert_pem = std::fs::read(&cert).unwrap();
        let key_pem = std::fs::read(&key).unwrap();
        let extras = CreativeWorkExtras {
            ipfs_cid: Some("QmTest123"),
            ..Default::default()
        };

        sign_image_with_materials_and_anchor(
            source.to_str().unwrap(),
            out.to_str().unwrap(),
            "IPFS Test",
            "Author",
            extras,
            None,
            &cert_pem,
            &key_pem,
        )
        .unwrap();

        let result = verify_image(out.to_str().unwrap()).unwrap();
        assert!(
            result.ipfs_cid.as_deref() == Some("ipfs://QmTest123"),
            "expected ipfs_cid ipfs://QmTest123, got: {:?}",
            result.ipfs_cid
        );

        let _ = std::fs::remove_file(&out);
    }

    #[test]
    fn sign_with_perceptual_hash_includes_assertion() {
        let base = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("assets")
            .join("test_data");
        let source = base.join("unsigned.jpg");
        let cert = base.join("es256_certs.pem");
        let key = base.join("es256_private.key");
        if !source.exists() || !cert.exists() || !key.exists() {
            return;
        }

        let out = std::env::temp_dir().join(format!(
            "trust_systems_phash_test_{}.jpg",
            std::process::id()
        ));
        let _ = std::fs::remove_file(&out);

        let cert_pem = std::fs::read(&cert).unwrap();
        let key_pem = std::fs::read(&key).unwrap();
        let extras = CreativeWorkExtras {
            perceptual_hash: Some("a1b2c3d4e5f67890"),
            ..Default::default()
        };

        sign_image_with_materials_and_anchor(
            source.to_str().unwrap(),
            out.to_str().unwrap(),
            "pHash Test",
            "Author",
            extras,
            None,
            &cert_pem,
            &key_pem,
        )
        .unwrap();

        let result = verify_image(out.to_str().unwrap()).unwrap();
        assert!(
            result.assertion_labels.contains(&PERCEPTUAL_HASH_ASSERTION_LABEL.to_string()),
            "expected assertion_labels to include perceptual-hash, got: {:?}",
            result.assertion_labels
        );

        let _ = std::fs::remove_file(&out);
    }

    #[test]
    fn software_signer_rejects_empty_paths() {
        assert!(SoftwareSigner::new("", "key.pem").is_err());
        assert!(SoftwareSigner::new("cert.pem", "   ").is_err());
    }

    #[test]
    fn tpm_signer_is_draft_and_returns_error() {
        let mut builder = Builder::from_json(r#"{"title":"Trust Systems"}"#)
            .map_err(|e| anyhow::anyhow!("{}", e))
            .unwrap();
        let tpm = TpmSigner;
        let err = tpm
            .sign(&mut builder, "/tmp/in.jpg", Path::new("/tmp/out.jpg"))
            .unwrap_err();
        assert!(err.to_string().contains("draft-only"));
    }

    #[test]
    fn sign_image_with_signer_surfaces_tpm_draft_error() {
        let input = env!("CARGO_MANIFEST_DIR").to_string() + "/assets/test_data/unsigned.jpg";
        if !std::path::Path::new(&input).exists() {
            return;
        }
        let out = std::env::temp_dir().join(format!(
            "trust_systems_tpm_draft_test_{}.jpg",
            std::process::id()
        ));
        let _ = std::fs::remove_file(&out);

        let tpm = TpmSigner;
        let err = sign_image_with_signer(
            &input,
            out.to_str().unwrap(),
            "Trust Systems",
            "Author",
            CreativeWorkExtras::default(),
            None,
            &tpm,
        )
        .unwrap_err();
        assert!(err.to_string().contains("draft-only"));
    }

    #[test]
    fn sign_image_fails_for_missing_input() {
        let err = sign_image(
            "/nonexistent/input.jpg",
            "/tmp/out.jpg",
            "assets/test_data/cert.pem",
            "assets/test_data/key.pem",
            "Author",
        )
        .unwrap_err();
        assert!(err.to_string().contains("not found"));
    }

    #[test]
    fn sign_image_fails_when_output_exists() {
        let input = env!("CARGO_MANIFEST_DIR").to_string() + "/assets/test_data/unsigned.jpg";
        if !std::path::Path::new(&input).exists() {
            return;
        }
        let out = std::env::temp_dir().join("trust_systems_test_output_exists.jpg");
        std::fs::write(&out, b"x").unwrap();
        let err = sign_image(
            &input,
            out.to_str().unwrap(),
            &(env!("CARGO_MANIFEST_DIR").to_string() + "/assets/test_data/cert.pem"),
            &(env!("CARGO_MANIFEST_DIR").to_string() + "/assets/test_data/key.pem"),
            "Author",
        )
        .unwrap_err();
        let _ = std::fs::remove_file(&out);
        assert!(err.to_string().contains("already exists"));
    }

    #[test]
    fn sign_image_rejects_empty_author() {
        let input = env!("CARGO_MANIFEST_DIR").to_string() + "/assets/test_data/unsigned.jpg";
        if !std::path::Path::new(&input).exists() {
            return;
        }
        let err = sign_image(
            &input,
            "/tmp/out.jpg",
            "assets/test_data/cert.pem",
            "assets/test_data/key.pem",
            "  ",
        )
        .unwrap_err();
        assert!(err.to_string().contains("author"));
    }

    #[test]
    fn sign_image_rejects_output_outside_home_and_temp() {
        let input = env!("CARGO_MANIFEST_DIR").to_string() + "/assets/test_data/unsigned.jpg";
        let cert = env!("CARGO_MANIFEST_DIR").to_string() + "/assets/test_data/cert.pem";
        let key = env!("CARGO_MANIFEST_DIR").to_string() + "/assets/test_data/key.pem";
        if !std::path::Path::new(&input).exists() || !std::path::Path::new(&cert).exists() {
            return;
        }
        // /etc is outside home and temp; output should be rejected
        let err = sign_image(
            &input,
            "/etc/trust_systems_test_out.jpg",
            &cert,
            &key,
            "Author",
        )
        .unwrap_err();
        assert!(
            err.to_string().contains("home") || err.to_string().contains("temp"),
            "expected path restriction error, got: {}",
            err
        );
    }

    /// Proves that signing an already-signed image with BuilderIntent::Edit appends a new manifest
    /// to the store and sets the previous manifest as the parent ingredient of the new one.
    #[test]
    fn manifest_chaining_edit_appends_manifest_and_sets_parent_ingredient() {
        let base = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("assets")
            .join("test_data");
        let raw_input = base.join("unsigned.jpg");
        let cert = base.join("es256_certs.pem");
        let key = base.join("es256_private.key");
        if !raw_input.exists() || !cert.exists() || !key.exists() {
            eprintln!(
                "Skipping manifest_chaining test: missing assets (unsigned.jpg, es256_certs.pem, es256_private.key)"
            );
            return;
        }

        let signer = SoftwareSigner::new(cert.to_str().unwrap(), key.to_str().unwrap()).unwrap();
        let step1 = std::env::temp_dir().join(format!(
            "trust_systems_chain_manifest_a_{}.jpg",
            std::process::id()
        ));
        let step2 = std::env::temp_dir().join(format!(
            "trust_systems_chain_manifest_b_{}.jpg",
            std::process::id()
        ));
        let _ = std::fs::remove_file(&step1);
        let _ = std::fs::remove_file(&step2);

        // Manifest A: sign raw image
        sign_image_with_signer(
            raw_input.to_str().unwrap(),
            step1.to_str().unwrap(),
            "Manifest A",
            "Author A",
            CreativeWorkExtras::default(),
            None,
            &signer,
        )
        .expect("first sign (Manifest A)");

        // Manifest B: sign the output of A (already has C2PA)
        sign_image_with_signer(
            step1.to_str().unwrap(),
            step2.to_str().unwrap(),
            "Manifest B",
            "Author B",
            CreativeWorkExtras::default(),
            None,
            &signer,
        )
        .expect("second sign (Manifest B)");

        // Inspect the final manifest store with c2pa::Reader
        let settings = Settings::new()
            .with_json(r#"{"verify": {"verify_trust": false}}"#)
            .map_err(|e| anyhow::anyhow!("{}", e))
            .unwrap();
        let reader = Reader::from_context(
            c2pa::Context::new()
                .with_settings(settings)
                .map_err(|e| anyhow::anyhow!("{}", e))
                .unwrap(),
        )
        .with_file(&step2)
        .map_err(|e| anyhow::anyhow!("{}", e))
        .expect("read final signed file");

        let manifest_count = reader.manifests().len();
        println!("Number of manifests in the store: {}", manifest_count);

        assert_eq!(
            manifest_count, 2,
            "store should contain exactly 2 manifests (A and B); got {}",
            manifest_count
        );

        let active = reader
            .active_manifest()
            .expect("final asset must have an active manifest");
        assert_eq!(
            active.title(),
            Some("Manifest B"),
            "active manifest should be Manifest B"
        );

        let parent = active.parent().expect(
            "Manifest B must have a parent ingredient referencing the previous manifest (Manifest A)",
        );
        println!(
            "Manifest B has parent ingredient: yes (title = {:?})",
            parent.title()
        );

        // Confirm Manifest A exists in the store (e.g. by title)
        let has_manifest_a = reader.iter_manifests().any(|m| m.title() == Some("Manifest A"));
        assert!(
            has_manifest_a,
            "store should contain Manifest A as well as Manifest B"
        );

        let _ = std::fs::remove_file(&step1);
        let _ = std::fs::remove_file(&step2);
    }

    #[test]
    fn dynamic_anchor_detection_matches_tag_uid_or_sun_cmac_keys() {
        let non_anchor = ManifestAssertion::new(
            "org.example.note".to_string(),
            serde_json::json!({ "foo": "bar" }),
        );
        let nested_anchor = ManifestAssertion::new(
            "org.example.anchor-like".to_string(),
            serde_json::json!({
                "meta": {
                    "nested": {
                        "tag_uid": "04AABBCCDD"
                    }
                }
            }),
        );

        let found =
            find_dynamic_anchor_assertion(&[non_anchor.clone(), nested_anchor.clone()]);
        assert!(found.is_some(), "expected to find anchor-shaped assertion");
        let (label, value) = found.unwrap();
        assert_eq!(label, nested_anchor.label());
        assert!(json_contains_anchor_keys(&value));

        let sun_cmac = ManifestAssertion::new(
            "org.example.anchor-sun".to_string(),
            serde_json::json!({ "sun_cmac": "A1B2C3" }),
        );
        let found_sun = find_dynamic_anchor_assertion(&[non_anchor, sun_cmac.clone()]);
        assert!(found_sun.is_some(), "expected to detect sun_cmac key");
        assert_eq!(found_sun.unwrap().0, sun_cmac.label());
    }

    #[test]
    fn is_charmed_false_for_non_charm_farm_certs() {
        let base = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("assets")
            .join("test_data");
        let source = base.join("sample.jpg");
        let cert = base.join("es256_certs.pem");
        let key = base.join("es256_private.key");
        if !source.exists() || !cert.exists() || !key.exists() {
            return;
        }
        let out = std::env::temp_dir().join(format!(
            "trust_systems_charmed_false_test_{}.jpg",
            std::process::id()
        ));
        let _ = std::fs::remove_file(&out);

        sign_image(
            source.to_str().unwrap(),
            out.to_str().unwrap(),
            cert.to_str().unwrap(),
            key.to_str().unwrap(),
            "Non-Charm Author",
        )
        .unwrap();

        let result = verify_image(out.to_str().unwrap()).unwrap();
        assert!(
            !result.is_charmed,
            "CAI test certs must not be charmed; issuer-text spoofing should not work"
        );
        assert_eq!(result.status, ValidationStatus::Success);

        let _ = std::fs::remove_file(&out);
    }

    #[test]
    fn c2pa_manifest_preserves_full_cert_chain() {
        let base = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("assets")
            .join("test_data");
        let source = base.join("sample.jpg");
        let cert = base.join("es256_certs.pem");
        let key = base.join("es256_private.key");
        if !source.exists() || !cert.exists() || !key.exists() {
            return;
        }
        let out = std::env::temp_dir().join(format!(
            "trust_systems_chain_preserve_test_{}.jpg",
            std::process::id()
        ));
        let _ = std::fs::remove_file(&out);

        let cert_pem = std::fs::read_to_string(&cert).expect("read cert chain");
        let input_cert_count = {
            let mut r = BufReader::new(cert_pem.as_bytes());
            certs(&mut r).collect::<Result<Vec<_>, _>>().unwrap().len()
        };
        assert!(
            input_cert_count >= 2,
            "test cert chain must have at least 2 certs; got {}",
            input_cert_count
        );

        sign_image(
            source.to_str().unwrap(),
            out.to_str().unwrap(),
            cert.to_str().unwrap(),
            key.to_str().unwrap(),
            "Chain Test Author",
        )
        .unwrap();

        let settings = Settings::new()
            .with_json(r#"{"verify": {"verify_trust": false}}"#)
            .map_err(|e| anyhow::anyhow!("{}", e))
            .unwrap();
        let reader = Reader::from_context(
            c2pa::Context::new()
                .with_settings(settings)
                .map_err(|e| anyhow::anyhow!("{}", e))
                .unwrap(),
        )
        .with_file(&out)
        .map_err(|e| anyhow::anyhow!("{}", e))
        .expect("read signed file");

        let manifest = reader.active_manifest().expect("active manifest");
        let sig = manifest.signature_info().expect("signature info");
        let embedded_chain = parse_cert_chain_ders(sig.cert_chain());

        println!(
            "Input certs: {}, Embedded certs in manifest: {}",
            input_cert_count,
            embedded_chain.len()
        );

        assert_eq!(
            embedded_chain.len(),
            input_cert_count,
            "c2pa manifest should embed the full cert chain ({} certs), but got {}",
            input_cert_count,
            embedded_chain.len()
        );

        let _ = std::fs::remove_file(&out);
    }

    #[test]
    fn charm_farm_anchor_fingerprint_is_stable() {
        let anchor_ders = parse_cert_chain_ders(ORG_TRUST_ANCHORS_PEM);
        assert_eq!(
            anchor_ders.len(),
            1,
            "charm_farm_root.pem should contain exactly one certificate"
        );
        let fp = cert_fingerprint_sha256_der(&anchor_ders[0]);
        assert!(
            !fp.is_empty(),
            "Charm Farm root fingerprint must be non-empty"
        );
        assert_eq!(
            fp.len(),
            64,
            "SHA-256 hex fingerprint must be 64 characters"
        );
    }

    #[test]
    fn transfer_asset_sets_publisher_and_verify_exposes_owner() {
        let base = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("assets")
            .join("test_data");
        let source = base.join("sample.jpg");
        let cert = base.join("es256_certs.pem");
        let key = base.join("es256_private.key");
        if !source.exists() || !cert.exists() || !key.exists() {
            return;
        }

        let original = std::env::temp_dir().join(format!(
            "trust_systems_transfer_owner_src_{}.jpg",
            std::process::id()
        ));
        let transferred = std::env::temp_dir().join(format!(
            "trust_systems_transfer_owner_dst_{}.jpg",
            std::process::id()
        ));
        let _ = std::fs::remove_file(&original);
        let _ = std::fs::remove_file(&transferred);

        sign_image(
            source.to_str().unwrap(),
            original.to_str().unwrap(),
            cert.to_str().unwrap(),
            key.to_str().unwrap(),
            "Original Author",
        )
        .expect("sign source image");

        let cert_pem = std::fs::read(cert).expect("read cert chain");
        let key_pem = std::fs::read(key).expect("read private key");
        transfer_asset_with_materials(
            original.to_str().unwrap(),
            transferred.to_str().unwrap(),
            "Recipient Alias",
            &cert_pem,
            &key_pem,
            None,
        )
        .expect("transfer image");

        let verify = verify_image(transferred.to_str().unwrap()).expect("verify transferred image");
        assert_eq!(verify.publisher.as_deref(), Some("Recipient Alias"));
        let cert_chain = std::str::from_utf8(&cert_pem).expect("cert chain utf8");
        let mut reader = BufReader::new(cert_chain.as_bytes());
        let parsed = certs(&mut reader)
            .collect::<std::result::Result<Vec<_>, _>>()
            .expect("parse signer cert chain");
        let mut hasher = Sha256::new();
        hasher.update(&parsed[0]);
        let expected_fp = hex::encode(hasher.finalize());
        assert_eq!(
            verify.signer_certificate_fingerprint.as_deref(),
            Some(expected_fp.as_str())
        );
        assert!(
            verify
                .assertion_labels
                .iter()
                .any(|l| l.starts_with(Actions::LABEL)),
            "expected actions assertion after transfer, got {:?}",
            verify.assertion_labels
        );

        let _ = std::fs::remove_file(&original);
        let _ = std::fs::remove_file(&transferred);
    }
}
