// Copyright 2024 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 the
// specific language governing permissions and limitations under
// each license.

use std::fmt::{self, Display};

use chrono::Utc;
#[cfg(feature = "json_schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::{
    assertion::AssertionBase,
    assertions::Ingredient,
    jumbf::labels::manifest_label_from_uri,
    status_tracker::{LogKind, StatusTracker},
    store::Store,
    validation_status::{self, log_kind, ValidationStatus},
};

/// Represents the levels of assurance a manifest store achieves when evaluated against the C2PA
/// specifications structural, cryptographic, and trust requirements.
///
/// See [Validation states - C2PA Technical Specification](https://spec.c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_validation_states).
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
pub enum ValidationState {
    /// The manifest store fails to meet ValidationState::WellFormed requirements, meaning it cannot
    /// even be parsed or its basic structure is non-compliant.
    ///
    /// This case may also occur if validation is disabled in the SDK.
    Invalid,
    /// The manifest store is well-formed and the cryptographic integrity checks succeed.
    ///
    /// See [Valid Manifest - C2PA Technical Specification](https://spec.c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_valid_manifest).
    Valid,
    /// The manifest store is valid and signed by a certificate that chains up to a trusted root or known
    /// authority in the trust list.
    ///
    /// See [Trusted Manifest - C2PA Technical Specification](https://spec.c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_trusted_manifest).
    Trusted,
}

/// Contains a set of success, informational, and failure validation status codes.
#[derive(Clone, Serialize, Default, Deserialize, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
pub struct StatusCodes {
    /// An array of validation success codes. May be empty.
    pub success: Vec<ValidationStatus>,
    /// An array of validation informational codes. May be empty.
    pub informational: Vec<ValidationStatus>,
    /// An array of validation failure codes. May be empty.
    pub failure: Vec<ValidationStatus>,
}

impl StatusCodes {
    /// Adds a [ValidationStatus] to the StatusCodes.
    pub fn add_status(&mut self, status: ValidationStatus) {
        match status.kind() {
            LogKind::Success => self.success.push(status),
            LogKind::Informational => self.informational.push(status),
            LogKind::Failure => self.failure.push(status),
        }
    }

    pub fn add_success_val(mut self, sm: ValidationStatus) -> Self {
        self.success.push(sm);
        self
    }

    pub fn success(&self) -> &Vec<ValidationStatus> {
        self.success.as_ref()
    }

    pub fn add_informational_val(mut self, sm: ValidationStatus) -> Self {
        self.informational.push(sm);
        self
    }

    pub fn informational(&self) -> &Vec<ValidationStatus> {
        self.informational.as_ref()
    }

    pub fn add_failure_val(mut self, sm: ValidationStatus) -> Self {
        self.failure.push(sm);
        self
    }

    pub fn failure(&self) -> &Vec<ValidationStatus> {
        self.failure.as_ref()
    }
}

/// A map of validation results for a manifest store.
///
/// The map contains the validation results for the active manifest and any ingredient deltas.
/// It is normal for there to be many
#[derive(Clone, Serialize, Default, Deserialize, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
pub struct ValidationResults {
    /// Validation status codes for the ingredient's active manifest. Present if ingredient is a C2PA
    /// asset. Not present if the ingredient is not a C2PA asset.
    #[serde(rename = "activeManifest", skip_serializing_if = "Option::is_none")]
    active_manifest: Option<StatusCodes>,

    /// List of any changes/deltas between the current and previous validation results for each ingredient's
    /// manifest. Present if the the ingredient is a C2PA asset.
    #[serde(rename = "ingredientDeltas", skip_serializing_if = "Option::is_none")]
    ingredient_deltas: Option<Vec<IngredientDeltaValidationResult>>,

    /// Time when the validation was performed (RFC 3339 date-time). Used only for document-level validationInfo; not serialized in validationResults (e.g. ingredient assertions).
    #[serde(rename = "validationTime", skip_serializing)]
    validation_time: Option<String>,
}

impl ValidationResults {
    pub(crate) fn from_store(store: &Store, validation_log: &StatusTracker) -> Self {
        let mut results = ValidationResults::default();

        let mut statuses: Vec<ValidationStatus> = validation_log
            .logged_items()
            .iter()
            .filter_map(ValidationStatus::from_log_item)
            .collect();

        // Filter out any status that is already captured in an ingredient assertion.
        // There is always an active manifest in a manifest store; ensure active_manifest is set
        // so serialization (e.g. crJSON) always includes activeManifest when validationResults exist.
        if let Some(claim) = store.provenance_claim() {
            let _ = results
                .active_manifest
                .get_or_insert_with(StatusCodes::default);
            let active_manifest = Some(claim.label().to_string());

            // This closure returns true if the URI references the store's active manifest.
            let is_active_manifest = |uri: Option<&str>| {
                uri.is_some_and(|uri| manifest_label_from_uri(uri) == active_manifest)
            };

            // Returns a flat list of validation statuses from the ingredient with absolute URIs.
            let get_statuses = |i: Ingredient| {
                // Get a flat list of validation statuses from the ingredient.
                // If validation_results are present, use them, otherwise use the ingredient's validation_status.
                //
                // the `kind` field is `#[serde(skip)]` so it doesn't survive serialization
                // of the ingredient. we also need to fix up `kind` since the older validation
                // statuses don't have it set.
                let validation_status: Option<Vec<ValidationStatus>> = i
                    .validation_results
                    .map(|v| v.validation_status())
                    .or(i.validation_status)
                    .map(|statuses| {
                        statuses
                            .into_iter()
                            .map(|s| {
                                let kind = log_kind(s.code());
                                s.set_kind(kind)
                            })
                            .collect()
                    });

                // Convert any relative manifest urls found in ingredient validation statuses to absolute.
                validation_status.map(|mut statuses| {
                    if let Some(label) = i
                        .active_manifest
                        .as_ref()
                        .or(i.c2pa_manifest.as_ref())
                        .map(|m| m.url())
                        .and_then(|uri| manifest_label_from_uri(&uri))
                    {
                        for status in &mut statuses {
                            status.make_absolute(&label)
                        }
                    }
                    statuses
                })
            };

            // We only need to do the more detailed filtering if there are any status
            // reports that reference ingredients.
            if statuses.iter().any(|s| !is_active_manifest(s.url())) {
                // Collect all the ValidationStatus records from all the ingredients in the store.
                // Since we need to process v1,v2 and v3 ingredients, we process all in the same format.
                let ingredient_statuses: Vec<ValidationStatus> = store
                    .claims()
                    .iter()
                    .flat_map(|c| c.ingredient_assertions())
                    .filter_map(|a| Ingredient::from_assertion(a.assertion()).ok())
                    .filter_map(get_statuses)
                    .flatten()
                    .collect();

                // Filter statuses to only contain those from the active manifest and those not found in any ingredient.
                statuses.retain(|s| {
                    is_active_manifest(s.url()) || !ingredient_statuses.iter().any(|i| i == s)
                })
            }
            for status in statuses {
                results.add_status(status);
            }
        }
        results.validation_time = Some(Utc::now().to_rfc3339());
        results
    }

    /// Returns the [ValidationState] of the manifest store based on the validation results.
    ///
    /// See [Validation states - C2PA Technical Specification](https://spec.c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_validation_states).
    pub fn validation_state(&self) -> ValidationState {
        if let Some(active_manifest) = self.active_manifest.as_ref() {
            // NOTE: Changes here may impact the impl of [`ValidationFailureSummary::fmt`].
            //       Ensure changes are reciprocated in both locations.
            //
            // A failure to trust the issuer of a CAWG identity claims aggregation
            // (ICA) credential is scoped to that individual identity assertion and
            // does not affect the enclosing manifest's state. It is recorded with
            // the informational code `cawg.ica.untrusted_issuer` (and the
            // credential's `cawg.ica.credential_valid` success code is withheld),
            // so it never appears among the failures examined below.
            //
            // https://spec.c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html#_valid_manifest
            let is_valid = active_manifest
                // First check if the claim is valid and the certificate hasn't expired.
                .success()
                .iter()
                .any(|status| status.code() == validation_status::CLAIM_SIGNATURE_VALIDATED)
                && active_manifest.success().iter().any(|status| {
                    status.code() == validation_status::CLAIM_SIGNATURE_INSIDE_VALIDITY
                })
                // Then check if the manifest contains either no failures or that it's only untrusted.
                && (active_manifest.failure().is_empty()
                    || active_manifest.failure().iter().all(|status| {
                        status.code() == validation_status::SIGNING_CREDENTIAL_UNTRUSTED
                    }))
                // Finally check if the ingredients contain either no failures or the only failure is
                // that the ingredient is untrusted.
                && self.ingredient_deltas.as_ref().iter().all(|deltas| {
                    deltas.iter().all(|idv| {
                        let deltas = idv.validation_deltas();
                        deltas.failure().is_empty()
                            || deltas.failure().iter().all(|status| {
                                status.code() == validation_status::SIGNING_CREDENTIAL_UNTRUSTED
                            })
                    })
                });

            // https://spec.c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html#_trusted_manifest
            let is_trusted = active_manifest
                // First check if the signing certificate is trusted.
                .success()
                .iter()
                .any(|status| status.code() == validation_status::SIGNING_CREDENTIAL_TRUSTED)
                // Then check that there are no errors.
                && active_manifest.failure().is_empty()
                // Finally check if the ingredients contain no failures.
                && self.ingredient_deltas.as_ref().iter().all(|deltas| {
                    deltas.iter().all(|idv| idv.validation_deltas().failure().is_empty())
                })
                && is_valid;

            if is_trusted {
                return ValidationState::Trusted;
            } else if is_valid {
                return ValidationState::Valid;
            }
        }

        ValidationState::Invalid
    }

    /// Returns a list of all validation errors in [ValidationResults].
    pub(crate) fn validation_errors(&self) -> Option<Vec<ValidationStatus>> {
        let mut status_vec = Vec::new();
        if let Some(active_manifest) = self.active_manifest.as_ref() {
            status_vec.extend(active_manifest.failure().to_vec());
        }
        if let Some(ingredient_deltas) = self.ingredient_deltas.as_ref() {
            for idv in ingredient_deltas.iter() {
                status_vec.extend(idv.validation_deltas().failure().to_vec());
            }
        }
        if status_vec.is_empty() {
            None
        } else {
            Some(status_vec)
        }
    }

    /// Returns a list of all validation status codes in [ValidationResults].
    pub(crate) fn validation_status(&self) -> Vec<ValidationStatus> {
        let mut status = Vec::new();
        if let Some(active_manifest) = self.active_manifest.as_ref() {
            status.extend(active_manifest.success().to_vec());
            status.extend(active_manifest.informational().to_vec());
            status.extend(active_manifest.failure().to_vec());
        }
        if let Some(ingredient_deltas) = self.ingredient_deltas.as_ref() {
            for idv in ingredient_deltas.iter() {
                status.extend(idv.validation_deltas().success().to_vec());
                status.extend(idv.validation_deltas().informational().to_vec());
                status.extend(idv.validation_deltas().failure().to_vec());
            }
        }
        status
    }

    /// Adds a [ValidationStatus] to the [ValidationResults].
    pub fn add_status(&mut self, status: ValidationStatus) -> &mut Self {
        match status.ingredient_uri() {
            None => {
                let scm = self
                    .active_manifest
                    .get_or_insert_with(StatusCodes::default);
                scm.add_status(status);
            }
            Some(ingredient_url) => {
                let ingredient_vec = self.ingredient_deltas.get_or_insert_with(Vec::new);
                match ingredient_vec
                    .iter_mut()
                    .find(|idv| idv.ingredient_assertion_uri() == ingredient_url)
                {
                    Some(idv) => {
                        idv.validation_deltas_mut().add_status(status);
                    }
                    None => {
                        let mut idv = IngredientDeltaValidationResult::new(
                            ingredient_url,
                            StatusCodes::default(),
                        );
                        idv.validation_deltas_mut().add_status(status);
                        ingredient_vec.push(idv);
                    }
                };
            }
        }
        self
    }

    /// Returns the active manifest status codes, if present.
    pub fn active_manifest(&self) -> Option<&StatusCodes> {
        self.active_manifest.as_ref()
    }

    /// Returns the ingredient deltas, if present.
    pub fn ingredient_deltas(&self) -> Option<&Vec<IngredientDeltaValidationResult>> {
        self.ingredient_deltas.as_ref()
    }

    /// Returns the time when validation was performed (RFC 3339), if set.
    pub fn validation_time(&self) -> Option<&str> {
        self.validation_time.as_deref()
    }

    pub fn add_active_manifest(mut self, scm: StatusCodes) -> Self {
        self.active_manifest = Some(scm);
        self
    }

    pub fn add_ingredient_delta(mut self, idv: IngredientDeltaValidationResult) -> Self {
        if let Some(id) = self.ingredient_deltas.as_mut() {
            id.push(idv);
        } else {
            self.ingredient_deltas = Some(vec![idv]);
        }
        self
    }

    /// Returns a summary of why validation failed.
    ///
    /// The `Display` impl outputs a human-readable view of the failures, suitable for error messages.
    /// If there are no failures, an empty string is output.
    pub fn failure_summary(&self) -> ValidationFailureSummary<'_> {
        ValidationFailureSummary(self)
    }

    pub(crate) fn format_status(status: &ValidationStatus, indent: &str) -> String {
        let mut line = format!("{indent}{}", status.code());
        if let Some(explanation) = status.explanation() {
            line.push_str(&format!(": {explanation}"));
        }
        if let Some(url) = status.url() {
            line.push_str(&format!(" ({url})"));
        }
        line
    }
}

impl Display for ValidationResults {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let state = self.validation_state();
        writeln!(f, "state: {state:?}")?;

        if let Some(active_manifest) = self.active_manifest.as_ref() {
            if !active_manifest.success.is_empty() {
                let codes = active_manifest
                    .success
                    .iter()
                    .map(|status| status.code())
                    .collect::<Vec<_>>();
                writeln!(f, "  success: {}", codes.join(", "))?;
            }
            if !active_manifest.informational.is_empty() {
                writeln!(f, "  informational:")?;
                for status in &active_manifest.informational {
                    writeln!(f, "{}", ValidationResults::format_status(status, "    "))?;
                }
            }
            if !active_manifest.failure.is_empty() {
                writeln!(f, "  failure:")?;
                for status in &active_manifest.failure {
                    writeln!(f, "{}", ValidationResults::format_status(status, "    "))?;
                }
            }
        }

        if let Some(deltas) = self.ingredient_deltas.as_ref() {
            for delta in deltas {
                let d = delta.validation_deltas();
                writeln!(f, "  ingredient [{}]:", delta.ingredient_assertion_uri())?;
                if !d.success.is_empty() {
                    let codes = d
                        .success
                        .iter()
                        .map(|status| status.code())
                        .collect::<Vec<_>>();
                    writeln!(f, "    success: {}", codes.join(", "))?;
                }
                if !d.informational.is_empty() {
                    writeln!(f, "    informational:")?;
                    for status in &d.informational {
                        writeln!(f, "{}", ValidationResults::format_status(status, "      "))?;
                    }
                }
                if !d.failure.is_empty() {
                    writeln!(f, "    failure:")?;
                    for status in &d.failure {
                        writeln!(f, "{}", ValidationResults::format_status(status, "      "))?;
                    }
                }
            }
        }

        Ok(())
    }
}

/// Concise display of why a [`ValidationResults`] is invalid (impls `Display`).
pub struct ValidationFailureSummary<'a>(&'a ValidationResults);

impl Display for ValidationFailureSummary<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let results = self.0;
        let mut output_lines = Vec::new();

        if let Some(active_manifest) = results.active_manifest.as_ref() {
            let failures = active_manifest
                .failure
                .iter()
                .filter(|status| status.code() != validation_status::SIGNING_CREDENTIAL_UNTRUSTED)
                .collect::<Vec<_>>();
            if !failures.is_empty() {
                output_lines.push("failures:".to_string());
                output_lines.extend(
                    failures
                        .iter()
                        .map(|status| ValidationResults::format_status(status, "  ")),
                );
            }
        }

        if let Some(deltas) = results.ingredient_deltas.as_ref() {
            for delta in deltas {
                let failures = delta
                    .validation_deltas()
                    .failure
                    .iter()
                    .filter(|status| {
                        status.code() != validation_status::SIGNING_CREDENTIAL_UNTRUSTED
                    })
                    .collect::<Vec<_>>();
                if !failures.is_empty() {
                    output_lines.push(format!(
                        "ingredient [{}] failures:",
                        delta.ingredient_assertion_uri()
                    ));
                    output_lines.extend(
                        failures
                            .iter()
                            .map(|status| ValidationResults::format_status(status, "  ")),
                    );
                }
            }
        }

        write!(f, "{}", output_lines.join("\n"))
    }
}

#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
/// Represents any changes or deltas between the current and previous validation results for an ingredient's manifest.
pub struct IngredientDeltaValidationResult {
    #[serde(rename = "ingredientAssertionURI")]
    /// JUMBF URI reference to the ingredient assertion
    ingredient_assertion_uri: String,
    #[serde(rename = "validationDeltas")]
    /// Validation results for the ingredient's active manifest
    validation_deltas: StatusCodes,
}

impl IngredientDeltaValidationResult {
    /// Creates a new [IngredientDeltaValidationResult] with the provided ingredient URI and validation deltas.
    pub fn new<S: Into<String>>(
        ingredient_assertion_uri: S,
        validation_deltas: StatusCodes,
    ) -> Self {
        IngredientDeltaValidationResult {
            ingredient_assertion_uri: ingredient_assertion_uri.into(),
            validation_deltas,
        }
    }

    pub fn ingredient_assertion_uri(&self) -> &str {
        self.ingredient_assertion_uri.as_str()
    }

    pub fn validation_deltas(&self) -> &StatusCodes {
        &self.validation_deltas
    }

    pub fn validation_deltas_mut(&mut self) -> &mut StatusCodes {
        &mut self.validation_deltas
    }
}

/// Implements validation status for specific parts of a manifest.
///
/// See [Standard Status Codes - C2PA Technical Specification](https://spec.c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_standard_status_codes).
pub mod validation_codes {
    use crate::status_tracker::LogKind;

    // -- success codes --

    /// The claim signature referenced in the ingredient's claim validated.
    ///
    /// Any corresponding URL should point to a C2PA claim signature box.
    pub const CLAIM_SIGNATURE_VALIDATED: &str = "claimSignature.validated";

    /// The claims signing certificate was valid at the time of signing.
    ///
    /// Any corresponding URL should point to a C2PA claim box.
    pub const CLAIM_SIGNATURE_INSIDE_VALIDITY: &str = "claimSignature.insideValidity";

    /// The signing credential is listed on the validator's trust list.
    ///
    /// Any corresponding URL should point to a C2PA claim signature box.
    pub const SIGNING_CREDENTIAL_TRUSTED: &str = "signingCredential.trusted";

    /// The signing credential for the manifest has not been revoked:
    ///
    /// Any corresponding URL should point to a C2PA claim
    pub const SIGNING_CREDENTIAL_NOT_REVOKED: &str = "signingCredential.ocsp.notRevoked";

    /// The time-stamp credential is well-formed and message imprint and validity
    /// are correct.
    ///
    /// Any corresponding URL should point to a C2PA claim signature box.
    pub const TIMESTAMP_VALIDATED: &str = "timeStamp.validated";

    /// The time-stamp credential is listed on the validator's trust list.
    ///
    /// Any corresponding URL should point to a C2PA claim signature box.
    pub const TIMESTAMP_TRUSTED: &str = "timeStamp.trusted";

    /// The hash of the the referenced assertion in the ingredient's manifest
    /// matches the corresponding hash in the assertion's hashed URI in the claim.
    ///
    /// Any corresponding URL should point to a C2PA assertion.
    pub const ASSERTION_HASHEDURI_MATCH: &str = "assertion.hashedURI.match";

    /// Hash of a byte range of the asset matches the hash declared in the
    /// data hash assertion.
    ///
    /// Any corresponding URL should point to a C2PA assertion.
    pub const ASSERTION_DATAHASH_MATCH: &str = "assertion.dataHash.match";

    /// Additional exclusions are present in the data hash assertion.
    ///
    /// Any corresponding URL should point to a C2PA assertion.
    pub const ASSERTION_DATAHASH_ADDITIONAL_EXCLUSIONS: &str =
        "assertion.dataHash.additionalExclusionsPresent";

    /// Hash of a box-based asset matches the hash declared in the BMFF
    /// hash assertion.
    ///
    /// Any corresponding URL should point to a C2PA assertion.
    pub const ASSERTION_BMFFHASH_MATCH: &str = "assertion.bmffHash.match";

    /// Hash of a box-based asset matches the hash declared in the General Box
    /// Hash assertion.
    ///
    /// Any corresponding URL should point to a C2PA assertion.
    pub const ASSERTION_BOXHASH_MATCH: &str = "assertion.boxesHash.match";

    /// Hash of all assets contained in collection match hashes declared
    /// in Collection Data
    /// Hash assertion.
    ///
    /// Any corresponding URL should point to a C2PA assertion.
    pub const ASSERTION_COLLECTIONHASH_MATCH: &str = "assertion.collectionHash.match";

    /// A non-embedded (remote) assertion was accessible at the time of
    /// validation.
    ///
    /// Any corresponding URL should point to a C2PA assertion.
    pub const ASSERTION_ACCESSIBLE: &str = "assertion.accessible";

    /// Hash of the ingredient's C2PA manifest was successfully validated.
    ///
    /// Any corresponding URL should point to a C2PA assertion.
    pub const INGREDIENT_MANIFEST_VALIDATED: &str = "ingredient.manifest.validated";

    /// Ingredient had no manifest.
    ///
    /// Any corresponding URL should point to a C2PA assertion.
    pub const INGREDIENT_PROVENANCE_UNKNOWN: &str = "ingredient.unknownProvenance";

    /// Hash of the ingredient’s C2PA Claim Signature box was successfully validated
    ///
    /// Any corresponding URL should point to a C2PA assertion.
    pub const INGREDIENT_CLAIM_SIGNATURE_VALIDATED: &str = "ingredient.claimSignature.validated";

    // -- informational codes --

    /// The validator chose not to perform an online OCSP check.
    ///
    /// Any corresponding URL should point to a C2PA claim signature box.
    pub const SIGNING_CREDENTIAL_OCSP_SKIPPED: &str = "signingCredential.ocsp.skipped";

    /// The validator attempted to perform an online OCSP check, but did not receive
    /// a response.
    ///
    /// Any corresponding URL should point to a C2PA claim signature box.
    pub const SIGNING_CREDENTIAL_OCSP_INACCESSIBLE: &str = "signingCredential.ocsp.inaccessible";

    /// The time-stamp does not correspond to the contents of the claim.
    ///
    /// Any corresponding URL should point to a C2PA claim signature box.
    pub const TIMESTAMP_MISMATCH: &str = "timeStamp.mismatch";

    /// The time-stamp does not correspond to the contents of the claim.
    ///
    /// Any corresponding URL should point to a C2PA claim signature box.
    pub const TIMESTAMP_MALFORMED: &str = "timeStamp.malformed";

    /// The signed time-stamp attribute in the signature falls outside the
    /// validity window of the signing certificate or the TSA's certificate.
    ///
    /// Any corresponding URL should point to a C2PA claim signature box.
    pub const TIMESTAMP_OUTSIDE_VALIDITY: &str = "timeStamp.outsideValidity";

    /// The time-stamp credential is not listed on the validator's trust list.
    ///
    /// Any corresponding URL should point to a C2PA claim signature box.
    pub const TIMESTAMP_UNTRUSTED: &str = "timeStamp.untrusted";

    /// The asset manifest cannot be interpreted by this version of the SDK.
    ///
    /// Any corresponding URL should point to a C2PA assertion.
    pub const MANIFEST_UNKNOWN_PROVENANCE: &str = "manifest.unknownProvenance";

    /// The manifest is not referenced via an ingredient assertion.
    ///
    /// Any corresponding URL should point to a C2PA assertion.
    pub const MANIFEST_UNREFERENCED: &str = "manifest.unreferenced";

    /// The algorithm has been deprecated.
    ///
    /// Any corresponding URL should point to a C2PA claim signature box.
    pub const ALGORITHM_DEPRECATED: &str = "algorithm.deprecated";

    /// The claimed time of signing (in the iat header of the signature)
    /// is within the validity period of the claim signer’s certificate
    /// chain and before the time in any corresponding trusted timestamp
    ///
    /// Any corresponding URL should point to a C2PA claim signature box.
    pub const TIME_OF_SIGNING_INSIDE_VALIDITY: &str = "timeOfSigning.insideValidity";

    // -- failure codes --

    /// The claim cbor is invalid
    ///
    /// Any corresponding URL should point to a C2PA claim box.
    pub const CLAIM_MALFORMED: &str = "claim.malformed";

    /// The referenced claim in the ingredient's manifest cannot be found.
    ///
    /// Any corresponding URL should point to a C2PA claim box.
    pub const CLAIM_MISSING: &str = "claim.missing";

    /// More than one claim box is present in the manifest.
    ///
    /// Any corresponding URL should point to a C2PA claim box.
    pub const CLAIM_MULTIPLE: &str = "claim.multiple";

    /// No hard bindings are present in the claim.
    ///
    /// Any corresponding URL should point to a C2PA claim box.
    pub const HARD_BINDINGS_MISSING: &str = "claim.hardBindings.missing";

    // Multiple hard bindings are present in the claim.
    ///
    /// Any corresponding URL should point to a C2PA assertion.
    pub const HARD_BINDINGS_MULTIPLE: &str = "assertion.multipleHardBindings";

    /// A required field is not present in the claim.
    ///
    /// Any corresponding URL should point to a C2PA claim box.
    pub const CLAIM_REQUIRED_MISSING: &str = "claim.required.missing";

    /// The cbor of the claim is not valid.
    ///
    /// Any corresponding URL should point to a C2PA claim box.
    pub const CLAIM_CBOR_INVALID: &str = "claim.cbor.invalid";

    /// The hash of the the referenced ingredient claim in the manifest
    /// does not match the corresponding hash in the ingredient's hashed
    /// URI in the claim.
    ///
    /// Any corresponding URL should point to a C2PA assertion.
    pub const INGREDIENT_HASHEDURI_MISMATCH: &str = "ingredient.hashedURI.mismatch";

    /// The claim signature referenced in the ingredient's claim
    /// cannot be found in its manifest.
    ///
    /// Any corresponding URL should point to a C2PA claim signature box.
    pub const CLAIM_SIGNATURE_MISSING: &str = "claimSignature.missing";

    /// The claim signature referenced in the ingredient's claim
    /// failed to validate.
    ///
    /// Any corresponding URL should point to a C2PA claim signature box.
    pub const CLAIM_SIGNATURE_MISMATCH: &str = "claimSignature.mismatch";

    /// If a manifest was documented to exist in a remote location,
    /// but is not present there, or the location is not currently available
    /// (such as in an offline scenario),
    /// the `manifest.inaccessible` error code shall be used to report the
    /// situation.
    ///
    /// `ValidationStatus.url()` URI reference to the C2PA Manifest that could not
    /// be accessed.
    pub const MANIFEST_INACCESSIBLE: &str = "manifest.inaccessible";

    /// The manifest has more than one ingredient whose `relationship`
    /// is `parentOf`.
    ///
    /// Any corresponding URL should point to a C2PA claim box.
    pub const MANIFEST_MULTIPLE_PARENTS: &str = "manifest.multipleParents";

    /// The manifest is an update manifest, but it contains hard binding
    /// or actions assertions.
    ///
    /// Any corresponding URL should point to a C2PA claim box.
    pub const MANIFEST_UPDATE_INVALID: &str = "manifest.update.invalid";

    /// The manifest is an update manifest, but it contains either zero
    /// or multiple `parentOf` ingredients.
    ///
    /// Any corresponding URL should point to a C2PA claim box.
    pub const MANIFEST_UPDATE_WRONG_PARENTS: &str = "manifest.update.wrongParents";

    /// The signing credential is not listed on the validator's trust list.
    ///
    /// Any corresponding URL should point to a C2PA claim signature box.
    pub const SIGNING_CREDENTIAL_UNTRUSTED: &str = "signingCredential.untrusted";

    /// The issuer of a CAWG identity claims aggregation (ICA) credential is not
    /// listed on the validator's configured list of trusted ICA issuers.
    ///
    /// This is informational and scoped to the individual CAWG identity
    /// assertion: it does not by itself render the enclosing C2PA manifest
    /// invalid or untrusted. The credential's `cawg.ica.credential_valid` success
    /// code is withheld when this is reported, so consumers should treat the
    /// absence of `cawg.ica.credential_valid` as the signal that the identity was
    /// not validated.
    ///
    /// Any corresponding URL should point to a CAWG identity assertion.
    pub const CAWG_ICA_UNTRUSTED_ISSUER: &str = "cawg.ica.untrusted_issuer";

    /// The signing credential is not valid for signing.
    ///
    /// Any corresponding URL should point to a C2PA claim signature box.
    pub const SIGNING_CREDENTIAL_INVALID: &str = "signingCredential.invalid";

    /// The signing credential has been revoked by the issuer.
    ///
    /// Any corresponding URL should point to a C2PA claim signature box.
    pub const SIGNING_CREDENTIAL_REVOKED: &str = "signingCredential.ocsp.revoked";

    /// The signing credential has expired.
    ///
    /// Any corresponding URL should point to a C2PA claim signature box.
    pub const SIGNING_CREDENTIAL_EXPIRED: &str = "signingCredential.expired";

    /// The hash of the the referenced assertion in the manifest does not
    /// match the corresponding hash in the assertion's hashed URI in the claim.
    ///
    /// Any corresponding URL should point to a C2PA assertion.
    pub const ASSERTION_HASHEDURI_MISMATCH: &str = "assertion.hashedURI.mismatch";

    /// An assertion listed in the ingredient's claim is missing from the
    /// ingredient's manifest.
    ///
    /// Any corresponding URL should point to a C2PA claim box.
    pub const ASSERTION_MISSING: &str = "assertion.missing";

    /// An assertion was found in the ingredient's manifest that was not
    /// explicitly declared in the ingredient's claim.
    ///
    /// Any corresponding URL should point to a C2PA claim box or assertion.
    pub const ASSERTION_UNDECLARED: &str = "assertion.undeclared";

    /// A non-embedded (remote) assertion was inaccessible at the time of
    /// validation.
    ///
    /// Any corresponding URL should point to a C2PA assertion.
    pub const ASSERTION_INACCESSIBLE: &str = "assertion.inaccessible";

    /// An assertion was declared as redacted in the ingredient's claim
    /// but is still present in the ingredient's manifest.
    ///
    /// Any corresponding URL should point to a C2PA assertion.
    pub const ASSERTION_NOT_REDACTED: &str = "assertion.notRedacted";

    /// An assertion was declared as redacted by its own claim.
    ///
    /// Any corresponding URL should point to a C2PA claim box.
    pub const ASSERTION_SELF_REDACTED: &str = "assertion.selfRedacted";

    /// A required field is not present in an assertion.
    ///
    /// Any corresponding URL should point to a C2PA assertion.
    pub const ASSERTION_REQUIRED_MISSING: &str = "assertion.required.missing";

    /// The JSON(-LD) of an assertion is not valid.
    ///
    /// Any corresponding URL should point to a C2PA assertion.
    pub const ASSERTION_JSON_INVALID: &str = "assertion.json.invalid";

    /// The cbor of an assertion is not valid.
    ///
    /// Any corresponding URL should point to a C2PA assertion.
    pub const ASSERTION_CBOR_INVALID: &str = "assertion.cbor.invalid";

    /// An action that requires an associated ingredient either does not have one
    /// or the one specified cannot be located
    ///
    /// Any corresponding URL should point to a C2PA assertion.
    pub const ACTION_ASSERTION_INGREDIENT_MISMATCH: &str = "assertion.action.ingredientMismatch";

    /// An `action` assertion was redacted when the ingredient's
    /// claim was created.
    ///
    /// Any corresponding URL should point to a C2PA assertion.
    pub const ACTION_ASSERTION_REDACTED: &str = "assertion.action.redacted";

    /// The hash of a byte range of the asset does not match the
    /// hash declared in the data hash assertion.
    ///
    /// Any corresponding URL should point to a C2PA assertion.
    pub const ASSERTION_DATAHASH_MISMATCH: &str = "assertion.dataHash.mismatch";

    /// The hash of a box-based asset does not match the hash declared
    /// in the BMFF hash assertion.
    ///
    /// Any corresponding URL should point to a C2PA assertion.
    pub const ASSERTION_BMFFHASH_MISMATCH: &str = "assertion.bmffHash.mismatch";

    /// The hash of a box-based asset does not match the hash declared
    /// in the General Boxes hash assertion.
    ///
    /// Any corresponding URL should point to a C2PA assertion.
    pub const ASSERTION_BOXHASH_MISMATCH: &str = "assertion.boxesHash.mismatch";

    /// The hash of a box-based asset does not contain boxes in the expected order
    /// for the General Boxes hash assertion.
    ///
    /// Any corresponding URL should point to a C2PA assertion.
    pub const ASSERTION_BOXHASH_UNKNOWN_BOX: &str = "assertion.boxesHash.unknownBox";

    /// A hard binding assertion is in a cloud data assertion.
    ///
    /// Any corresponding URL should point to a C2PA assertion.
    pub const ASSERTION_CLOUD_DATA_HARD_BINDING: &str = "assertion.cloud-data.hardBinding";

    /// An update manifest contains a cloud data assertion referencing
    /// an actions assertion.
    ///
    /// Any corresponding URL should point to a C2PA assertion.
    pub const ASSERTION_CLOUD_DATA_ACTIONS: &str = "assertion.cloud-data.actions";

    /// The value of an `alg` header, or other header that specifies an
    /// algorithm used to compute the value of another field, is unknown
    /// or unsupported.
    ///
    /// Any corresponding URL should point to a C2PA claim box or C2PA assertion.
    pub const ALGORITHM_UNSUPPORTED: &str = "algorithm.unsupported";

    /// A value to be used when there was an error not specifically listed here.
    ///
    /// Any corresponding URL should point to a C2PA claim box or C2PA assertion.
    pub const GENERAL_ERROR: &str = "general.error";

    /// The claim signature referenced in the claim was created outside the validity
    /// period of the signing credential
    ///
    /// Any corresponding URL should point to a C2PA claim signature box.
    pub const CLAIM_SIGNATURE_OUTSIDE_VALIDITY: &str = "claimSignature.outsideValidity";

    /// The manifest is a time-stamp manifest, but it contains a
    /// disallowed (non-ingredient) assertion.
    ///
    /// Any corresponding URL should point to a C2PA claim  box.
    pub const MANIFEST_TIMESTAMP_INVALID: &str = "manifest.timestamp.invalid";

    ///The manifest is an time-stamp manifest, but it contains either zero or
    ///  multiple parentOf ingredients.
    ///
    /// Any corresponding URL should point to a C2PA claim box.
    pub const MANIFEST_TIMESTAMP_WRONG_PARENTS: &str = "manifest.timestamp.wrongParents";

    /// The compressed manifest was not valid.
    ///
    /// Any corresponding URL should point to a C2PA claim box.
    pub const MANIFEST_COMPRESSED_INVALID: &str = "manifest.compressed.invalid";

    /// The OCSP response contains an unknown status for the signing credential.
    ///
    /// Any corresponding URL should point to a C2PA claim signature box.
    pub const SIGNING_CREDENTIAL_OCSP_UNKNOWN: &str = "signingCredential.ocsp.unknown";

    /// An assertion listed in the claim is not in the same C2PA Manifest as
    /// the claim.
    ///
    /// Any corresponding URL should point to a C2PA claim  box.
    pub const ASSERTION_OUTSIDE_MANIFEST: &str = "assertion.outsideManifest";

    /// An actions assertion is malformed.
    ///
    /// Any corresponding URL should point to a C2PA assertion box.
    pub const ASSERTION_ACTION_MALFORMED: &str = "assertion.action.malformed";

    /// An actions assertion ingredient malformed.
    ///
    /// Any corresponding URL should point to a C2PA assertion box.
    pub const ASSERTION_ACTION_INGREDIENT_MISMATCH: &str = "assertion.action.ingredientMismatch";

    /// An action that requires an associated redaction either does not have one
    ///  or the one specified cannot be located
    ///
    /// Any corresponding URL should point to a C2PA assertion box.
    pub const ASSERTION_ACTION_REDACTION_MISMATCH: &str = "assertion.action.redactionMismatch";

    /// An actions assertion was redacted when the claim was created.
    ///
    /// Any corresponding URL should point to a C2PA assertion.
    pub const ASSERTION_ACTION_REDACTED: &str = "assertion.action.redacted";

    /// A data hash assertion is malformed.
    ///
    /// Any corresponding URL should point to a C2PA assertion box.
    pub const ASSERTION_DATAHASH_MALFORMED: &str = "assertion.dataHash.malformed";

    /// A hard binding assertion was redacted when the claim was created.
    ///
    /// Any corresponding URL should point to a C2PA assertion box.
    pub const ASSERTION_DATAHASH_REDACTED: &str = "assertion.dataHash.redacted";

    /// A BMFF hash assertion is malformed.
    ///
    /// Any corresponding URL should point to a C2PA assertion box.
    pub const ASSERTION_BMFFHASH_MALFORMED: &str = "assertion.bmffHash.malformed";

    /// A Box hash assertion is malformed.
    ///
    /// Any corresponding URL should point to a C2PA assertion box.
    pub const ASSERTION_BOXESHASH_MALFORMED: &str = "assertion.boxesHash.malformed";

    /// The cloud-data assertion was incomplete.
    ///
    /// Any corresponding URL should point to a C2PA assertion box.
    pub const ASSERTION_CLOUD_DATA_MALFORMED: &str = "assertion.cloud-data.malformed";

    /// A hash of an asset in the collection does not match hash declared in
    /// the collection data hash assertion.
    ///
    /// Any corresponding URL should point to a C2PA assertion box.
    pub const ASSERTION_COLLECTIONHASH_MISMATCH: &str = "assertion.collectionHash.mismatch";

    /// An asset that was listed in the collection data hash assertion is
    /// missing from the collection.
    ///
    /// Any corresponding URL should point to a C2PA assertion box.
    pub const ASSERTION_COLLECTIONHASH_INCORRECT_FILE_COUNT: &str =
        "assertion.collectionHash.incorrectFileCount";

    /// A URI of an asset in the collection data hash assertion contains
    /// the file part '..' or '.'.
    ///
    /// Any corresponding URL should point to a C2PA assertion box.
    pub const ASSERTION_COLLECTIONHASH_INVALID_URI: &str = "assertion.collectionHash.invalidURI";

    /// The collection hash assertion was incomplete.
    ///
    /// Any corresponding URL should point to a C2PA assertion box.
    pub const ASSERTION_COLLECTIONHASH_MALFORMED: &str = "assertion.collectionHash.malformed";

    /// The ingredient assertion was incomplete.
    ///
    /// Any corresponding URL should point to a C2PA assertion box.
    pub const ASSERTION_INGREDIENT_MALFORMED: &str = "assertion.ingredient.malformed";

    /// The C2PA metadata assertion contains a field that is not
    /// allowed by this specification.
    ///
    /// Any corresponding URL should point to a C2PA assertion box.
    pub const ASSERTION_METADATA_DISALLOWED: &str = "assertion.metadata.disallowed";

    /// The referenced ingredient C2PA Claim Signature was not found.
    ///
    /// Any corresponding URL should point to a C2PA assertion box.
    pub const INGREDIENT_MANIFEST_MISSING: &str = "ingredient.manifest.missing";

    /// The hash of an embedded C2PA Manifest does not match the hash declared in
    /// the hashed_uri value of the activeManifest field in the ingredient
    /// assertion.
    ///
    /// Any corresponding URL should point to a C2PA assertion box.
    pub const INGREDIENT_MANIFEST_MISMATCH: &str = "ingredient.manifest.mismatch";

    /// The referenced ingredient C2PA Claim Signature was not found.
    ///
    /// Any corresponding URL should point to a C2PA assertion box.
    pub const INGREDIENT_CLAIM_SIGNATURE_MISSING: &str = "ingredient.claimSignature.missing";

    /// The hash of an embedded C2PA Manifest’s C2PA Claim Signature does not match
    /// the hash declared in the hashed_uri value of the claimSignature field in the
    /// ingredient assertion.
    ///
    /// Any corresponding URL should point to a C2PA assertion box.
    pub const INGREDIENT_CLAIM_SIGNATURE_MISMATCH: &str = "ingredient.claimSignature.mismatch";

    /// The data pointed to by a hashed_uri cannot be located.
    ///
    /// Any corresponding URL should point to a C2PA assertion box.
    pub const HASHED_URI_MISSING: &str = "hashedURI.missing";

    /// The hash of a given hashed_uri does not match the corresponding hash
    /// of the destination URI’s data
    ///
    /// Any corresponding URL should point to a C2PA assertion box.
    pub const HASHED_URI_MISMATCH: &str = "hashedURI.mismatch";

    /// The timestamp assertion is malformed.
    ///
    /// Any corresponding URL should point to a C2PA assertion box.
    pub const ASSERTION_TIMESTAMP_MALFORMED: &str = "assertion.timestamp.malformed";

    /// Returns `true` if the status code is a known C2PA success status code.
    ///
    /// Returns `false` if the status code is a known C2PA failure status
    /// code or is unknown.
    ///
    /// ## Examples
    ///
    /// ```
    /// use c2pa::validation_results::validation_codes::*;
    ///
    /// assert!(is_success(CLAIM_SIGNATURE_VALIDATED));
    /// assert!(!is_success(SIGNING_CREDENTIAL_REVOKED));
    /// ```
    pub fn is_success(status_code: &str) -> bool {
        matches!(log_kind(status_code), LogKind::Success)
    }

    /// Returns the [`LogKind`] for a given status code.
    // TODO: This needs to be expanded to include all status codes.
    pub fn log_kind(status_code: &str) -> LogKind {
        match status_code {
            CLAIM_SIGNATURE_VALIDATED
            | CLAIM_SIGNATURE_INSIDE_VALIDITY
            | SIGNING_CREDENTIAL_TRUSTED
            | SIGNING_CREDENTIAL_NOT_REVOKED
            | TIMESTAMP_TRUSTED
            | TIMESTAMP_VALIDATED
            | ASSERTION_HASHEDURI_MATCH
            | ASSERTION_DATAHASH_MATCH
            | ASSERTION_BMFFHASH_MATCH
            | ASSERTION_ACCESSIBLE
            | ASSERTION_BOXHASH_MATCH
            | ASSERTION_COLLECTIONHASH_MATCH
            | INGREDIENT_MANIFEST_VALIDATED
            | INGREDIENT_MANIFEST_MISSING
            | INGREDIENT_CLAIM_SIGNATURE_VALIDATED => LogKind::Success,
            SIGNING_CREDENTIAL_OCSP_SKIPPED
            | SIGNING_CREDENTIAL_OCSP_INACCESSIBLE
            | SIGNING_CREDENTIAL_OCSP_UNKNOWN
            | TIMESTAMP_UNTRUSTED
            | TIMESTAMP_OUTSIDE_VALIDITY
            | TIMESTAMP_MISMATCH
            | TIMESTAMP_MALFORMED
            | MANIFEST_UNKNOWN_PROVENANCE
            | ALGORITHM_DEPRECATED
            | TIME_OF_SIGNING_INSIDE_VALIDITY
            | INGREDIENT_PROVENANCE_UNKNOWN
            | ASSERTION_DATAHASH_ADDITIONAL_EXCLUSIONS
            | CAWG_ICA_UNTRUSTED_ISSUER => LogKind::Informational,
            _ => LogKind::Failure,
        }
    }
}

#[cfg(test)]
pub mod tests {
    #![allow(clippy::unwrap_used)]

    use super::*;
    use crate::{
        assertions,
        claim::Claim,
        jumbf::labels,
        log_item,
        validation_status::{
            ASSERTION_DATAHASH_MISMATCH, ASSERTION_HASHEDURI_MISMATCH, CLAIM_MALFORMED,
            CLAIM_SIGNATURE_INSIDE_VALIDITY, CLAIM_SIGNATURE_VALIDATED, SIGNING_CREDENTIAL_TRUSTED,
            SIGNING_CREDENTIAL_UNTRUSTED,
        },
        HashedUri, Relationship,
    };

    #[test]
    fn trusted_state() {
        let mut validation_results = ValidationResults::default();

        validation_results.add_status(
            ValidationStatus::new(CLAIM_SIGNATURE_VALIDATED).set_kind(LogKind::Success),
        );
        validation_results.add_status(
            ValidationStatus::new(CLAIM_SIGNATURE_INSIDE_VALIDITY).set_kind(LogKind::Success),
        );
        validation_results.add_status(
            ValidationStatus::new(SIGNING_CREDENTIAL_TRUSTED).set_kind(LogKind::Success),
        );

        assert_eq!(
            validation_results.validation_state(),
            ValidationState::Trusted
        );
    }

    #[test]
    fn not_trusted_state_with_failure() {
        let mut validation_results = ValidationResults::default();

        validation_results.add_status(
            ValidationStatus::new(CLAIM_SIGNATURE_VALIDATED).set_kind(LogKind::Success),
        );
        validation_results.add_status(
            ValidationStatus::new(CLAIM_SIGNATURE_INSIDE_VALIDITY).set_kind(LogKind::Success),
        );
        validation_results.add_status(
            ValidationStatus::new(SIGNING_CREDENTIAL_TRUSTED).set_kind(LogKind::Success),
        );

        validation_results.add_status(ValidationStatus::new_failure(SIGNING_CREDENTIAL_UNTRUSTED));

        assert_eq!(
            validation_results.validation_state(),
            ValidationState::Valid
        );
    }

    #[test]
    fn not_trusted_state_with_failure_delta() {
        let mut validation_results = ValidationResults::default();

        validation_results.add_status(
            ValidationStatus::new(CLAIM_SIGNATURE_VALIDATED).set_kind(LogKind::Success),
        );
        validation_results.add_status(
            ValidationStatus::new(CLAIM_SIGNATURE_INSIDE_VALIDITY).set_kind(LogKind::Success),
        );
        validation_results.add_status(
            ValidationStatus::new(SIGNING_CREDENTIAL_TRUSTED).set_kind(LogKind::Success),
        );

        validation_results.add_status(
            ValidationStatus::new_failure(SIGNING_CREDENTIAL_UNTRUSTED).set_ingredient_uri("1"),
        );

        assert_eq!(
            validation_results.validation_state(),
            ValidationState::Valid
        );
    }

    #[test]
    fn valid_state() {
        let mut validation_results = ValidationResults::default();

        validation_results.add_status(
            ValidationStatus::new(CLAIM_SIGNATURE_VALIDATED).set_kind(LogKind::Success),
        );
        validation_results.add_status(
            ValidationStatus::new(CLAIM_SIGNATURE_INSIDE_VALIDITY).set_kind(LogKind::Success),
        );

        assert_eq!(
            validation_results.validation_state(),
            ValidationState::Valid
        );
    }

    #[test]
    fn valid_state_with_untrusted_delta() {
        let mut validation_results = ValidationResults::default();

        validation_results.add_status(
            ValidationStatus::new(CLAIM_SIGNATURE_VALIDATED).set_kind(LogKind::Success),
        );
        validation_results.add_status(
            ValidationStatus::new(CLAIM_SIGNATURE_INSIDE_VALIDITY).set_kind(LogKind::Success),
        );

        validation_results.add_status(
            ValidationStatus::new_failure(SIGNING_CREDENTIAL_UNTRUSTED).set_ingredient_uri("1"),
        );

        assert_eq!(
            validation_results.validation_state(),
            ValidationState::Valid
        );
    }

    #[test]
    fn not_valid_state_with_failure() {
        let mut validation_results = ValidationResults::default();

        validation_results.add_status(
            ValidationStatus::new(CLAIM_SIGNATURE_VALIDATED).set_kind(LogKind::Success),
        );
        validation_results.add_status(
            ValidationStatus::new(CLAIM_SIGNATURE_INSIDE_VALIDITY).set_kind(LogKind::Success),
        );

        validation_results.add_status(ValidationStatus::new_failure(CLAIM_MALFORMED));

        assert_eq!(
            validation_results.validation_state(),
            ValidationState::Invalid
        );
    }

    #[test]
    fn valid_state_with_failure_delta_and_untrusted_delta() {
        let mut validation_results = ValidationResults::default();

        validation_results.add_status(
            ValidationStatus::new(CLAIM_SIGNATURE_VALIDATED).set_kind(LogKind::Success),
        );
        validation_results.add_status(
            ValidationStatus::new(CLAIM_SIGNATURE_INSIDE_VALIDITY).set_kind(LogKind::Success),
        );

        validation_results.add_status(
            ValidationStatus::new_failure(SIGNING_CREDENTIAL_UNTRUSTED).set_ingredient_uri("1"),
        );
        validation_results.add_status(
            ValidationStatus::new_failure(ASSERTION_DATAHASH_MISMATCH).set_ingredient_uri("1"),
        );

        assert_eq!(
            validation_results.validation_state(),
            ValidationState::Invalid
        );
    }

    #[test]
    fn not_valid_state_with_failure_delta() {
        let mut validation_results = ValidationResults::default();

        validation_results.add_status(
            ValidationStatus::new(CLAIM_SIGNATURE_VALIDATED).set_kind(LogKind::Success),
        );
        validation_results.add_status(
            ValidationStatus::new(CLAIM_SIGNATURE_INSIDE_VALIDITY).set_kind(LogKind::Success),
        );

        validation_results.add_status(
            ValidationStatus::new_failure(ASSERTION_DATAHASH_MISMATCH).set_ingredient_uri("1"),
        );

        assert_eq!(
            validation_results.validation_state(),
            ValidationState::Invalid
        );
    }

    #[test]
    fn not_valid_state_with_no_inside_validity() {
        let mut validation_results = ValidationResults::default();

        validation_results.add_status(
            ValidationStatus::new(CLAIM_SIGNATURE_VALIDATED).set_kind(LogKind::Success),
        );

        assert_eq!(
            validation_results.validation_state(),
            ValidationState::Invalid
        );
    }

    #[test]
    fn not_valid_state_with_no_validated() {
        let mut validation_results = ValidationResults::default();

        validation_results.add_status(
            ValidationStatus::new(CLAIM_SIGNATURE_INSIDE_VALIDITY).set_kind(LogKind::Success),
        );

        assert_eq!(
            validation_results.validation_state(),
            ValidationState::Invalid
        );
    }

    #[test]
    fn invalid_state() {
        let mut validation_results = ValidationResults::default();

        validation_results.add_status(ValidationStatus::new_failure(ASSERTION_DATAHASH_MISMATCH));

        assert_eq!(
            validation_results.validation_state(),
            ValidationState::Invalid
        );
    }

    #[test]
    fn invalid_state_with_nothing() {
        let validation_results = ValidationResults::default();
        assert_eq!(
            validation_results.validation_state(),
            ValidationState::Invalid
        );
    }

    #[test]
    fn failure_summary_active_manifest_failure() {
        let mut validation_results = ValidationResults::default();
        validation_results.add_status(ValidationStatus::new_failure(ASSERTION_DATAHASH_MISMATCH));

        assert_eq!(
            validation_results.validation_state(),
            ValidationState::Invalid
        );
        assert_eq!(
            validation_results.failure_summary().to_string(),
            "failures:\n  assertion.dataHash.mismatch"
        );
    }

    #[test]
    fn failure_summary_ingredient_failure() {
        let mut validation_results = ValidationResults::default();
        validation_results.add_status(
            ValidationStatus::new(CLAIM_SIGNATURE_VALIDATED).set_kind(LogKind::Success),
        );
        validation_results.add_status(
            ValidationStatus::new(CLAIM_SIGNATURE_INSIDE_VALIDITY).set_kind(LogKind::Success),
        );
        validation_results.add_status(
            ValidationStatus::new_failure(ASSERTION_DATAHASH_MISMATCH)
                .set_ingredient_uri("urn:uuid:1234"),
        );

        assert_eq!(
            validation_results.validation_state(),
            ValidationState::Invalid
        );
        assert_eq!(
            validation_results.failure_summary().to_string(),
            "ingredient [urn:uuid:1234] failures:\n  assertion.dataHash.mismatch"
        );
    }

    #[test]
    fn failure_summary_filters_untrusted_ingredient() {
        let mut validation_results = ValidationResults::default();
        validation_results.add_status(
            ValidationStatus::new(CLAIM_SIGNATURE_VALIDATED).set_kind(LogKind::Success),
        );
        validation_results.add_status(
            ValidationStatus::new(CLAIM_SIGNATURE_INSIDE_VALIDITY).set_kind(LogKind::Success),
        );
        validation_results.add_status(
            ValidationStatus::new_failure(SIGNING_CREDENTIAL_UNTRUSTED)
                .set_ingredient_uri("urn:uuid:1234"),
        );

        assert_eq!(
            validation_results.validation_state(),
            ValidationState::Valid
        );
        assert_eq!(validation_results.failure_summary().to_string(), "");
    }

    #[test]
    fn validation_results_display() {
        let mut validation_results = ValidationResults::default();
        validation_results.add_status(
            ValidationStatus::new(CLAIM_SIGNATURE_VALIDATED).set_kind(LogKind::Success),
        );
        validation_results.add_status(
            ValidationStatus::new(CLAIM_SIGNATURE_INSIDE_VALIDITY).set_kind(LogKind::Success),
        );
        validation_results.add_status(
            ValidationStatus::new(validation_status::TIMESTAMP_MALFORMED)
                .set_kind(LogKind::Informational),
        );
        validation_results.add_status(
            ValidationStatus::new_failure(CLAIM_MALFORMED)
                .set_url("self#jumbf=c2pa/urn:uuid:abc/c2pa.claim")
                .set_explanation("claim CBOR failed to decode".to_string()),
        );
        validation_results.add_status(
            ValidationStatus::new_failure(ASSERTION_DATAHASH_MISMATCH)
                .set_ingredient_uri("urn:uuid:abcd"),
        );

        assert_eq!(
            validation_results.validation_state(),
            ValidationState::Invalid
        );
        assert_eq!(
            validation_results.to_string(),
            concat!(
                "state: Invalid\n",
                "  success: claimSignature.validated, claimSignature.insideValidity\n",
                "  informational:\n",
                "    timeStamp.malformed\n",
                "  failure:\n",
                "    claim.malformed: claim CBOR failed to decode (self#jumbf=c2pa/urn:uuid:abc/c2pa.claim)\n",
                "  ingredient [urn:uuid:abcd]:\n",
                "    failure:\n",
                "      assertion.dataHash.mismatch\n",
            )
        );
    }

    #[test]
    fn from_store_attested_ingredient_failure_after_serde_roundtrip() {
        let inner_label = "urn:uuid:inner-test";
        let inner_manifest_uri = labels::to_manifest_uri(inner_label);
        let assertion_url = format!("{inner_manifest_uri}/c2pa.assertions/org.test.data");

        let mut attested = ValidationResults::default();
        attested.add_status(
            ValidationStatus::new_failure(ASSERTION_HASHEDURI_MISMATCH).set_url(&assertion_url),
        );

        let ingredient = Ingredient {
            relationship: Relationship::ComponentOf,
            version: 3,
            active_manifest: Some(HashedUri::new(
                inner_manifest_uri.clone(),
                Some("sha256".into()),
                &[0u8; 32],
            )),
            validation_results: Some(attested),
            ..Default::default()
        };

        let mut outer_claim = Claim::new("test-generator", None, 2);
        outer_claim.add_assertion(&ingredient).unwrap();
        let outer_label = outer_claim.label().to_string();

        let mut store = Store::new();
        store.insert_restored_claim(outer_label.clone(), outer_claim);

        let mut tracker = StatusTracker::default();
        tracker.push_ingredient_uri(labels::to_assertion_uri(
            &outer_label,
            assertions::labels::INGREDIENT,
        ));
        let _ = log_item!(
            assertion_url.clone(),
            "hash does not match assertion data",
            "verify_internal"
        )
        .validation_status(ASSERTION_HASHEDURI_MISMATCH)
        .failure(&mut tracker, "hash mismatch");
        tracker.pop_ingredient_uri();

        let results = ValidationResults::from_store(&store, &tracker);

        let delta_failures: Vec<_> = results
            .ingredient_deltas
            .as_ref()
            .map(|deltas| {
                deltas
                    .iter()
                    .flat_map(|deltas| deltas.validation_deltas().failure().iter())
                    .collect()
            })
            .unwrap_or_default();

        // check that there are no failures since they were attested to
        assert!(delta_failures.is_empty());
    }
}
