use std::borrow::Cow;
use std::fmt::{self, Display, Formatter, Write};

use serde::Serialize;
use serde_json::Value;

use crate::format::{format_option_string, make_diff};
use crate::severity::Severity;

/// List of differences between two crates
#[derive(Debug, Default)]
pub struct Diff {
    items: Vec<DiffItem>,
}

impl Diff {
    pub(crate) const fn from_items(items: Vec<DiffItem>) -> Self {
        Diff { items }
    }

    /// List of differences
    #[must_use]
    pub fn items(&self) -> &[DiffItem] {
        &self.items
    }

    /// Get list of differences in machine-readable JSON format
    ///
    /// # Panics
    ///
    /// This function panics if there are internal errors related to serializing
    /// data in JSON format.
    #[must_use]
    pub fn to_json(&self) -> String {
        let items: Vec<JsonDiffItem> = self
            .items
            .iter()
            .map(|i| JsonDiffItem {
                severity: i.severity().to_string(),
                kind: i.kind(),
                data: i.data(),
            })
            .collect();

        // if this fails, something is seriously wrong - just panic
        #[expect(clippy::expect_used)]
        serde_json::to_string_pretty(&items).expect("Failed to serialize report as JSON.")
    }
}

impl Display for Diff {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        for item in self.items() {
            if f.alternate() {
                write!(f, "{item:#}")?;
            } else {
                write!(f, "{item}")?;
            }
        }
        Ok(())
    }
}

/// A specific difference between two crates
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
// keep in sync with the Python enum
pub enum DiffItem {
    // file content changes
    /// A file was added (it is present in the new version but not the old version).
    FileAdded {
        /// Path of the added file
        path: String,
    },
    /// A file was removed (it is present in the old version but not the new version).
    FileRemoved {
        /// Path of the removed file
        path: String,
    },
    /// Contents of a file have changed.
    FileChanged {
        /// Path of the file
        path: String,
        /// Diff between old and new contents of the file
        ///
        /// If this is `None` then the file is a binary file and / or not valid UTF-8.
        diff: Option<String>,
    },
    /// Contents of a file have changed *only* due to change in the line endings style.
    LineEndingsChange {
        /// Path of the file
        path: String,
    },
    /// File mode / permissions have changed between old and new version.
    PermissionChange {
        /// Path of the file
        path: String,
        /// Old permission bits
        old: String,
        /// New permission bits
        new: String,
    },

    // metadata changes
    /// The crate name has changed.
    NameChange {
        /// Old crate name
        old: String,
        /// New crate name
        new: String,
    },
    /// The crate version has changed.
    VersionChange {
        /// Old crate version
        old: String,
        /// New crate version
        new: String,
    },
    /// The crate edition has changed.
    EditionChange {
        /// Old crate Edition
        old: String,
        /// New crate Edition
        new: String,
    },
    /// The crate MSRV has changed.
    RustVersionChange {
        /// Old crate MSRV
        old: Option<String>,
        /// New crate MSRV
        new: Option<String>,
    },
    /// The list of crate authors has changed.
    AuthorsChange {
        /// Authors added in the new version
        added: Vec<String>,
        /// Authors removed in the new version
        removed: Vec<String>,
    },
    /// The crate description has changed.
    DescriptionChange {
        /// Old crate description
        old: Option<String>,
        /// New crate description
        new: Option<String>,
    },
    /// The crate license has changed.
    LicenseChange {
        /// Old crate license expression
        old: Option<String>,
        /// New crate license expression
        new: Option<String>,
    },
    /// The crate license file path has changed.
    LicenseFileChange {
        /// Old license file path
        old: Option<String>,
        /// New license file path
        new: Option<String>,
    },
    /// The crate readme file path has changed.
    ReadmeChange {
        /// Old readme file path
        old: Option<String>,
        /// New readme file path
        new: Option<String>,
    },
    /// The list of crate categories has changed.
    CategoriesChange {
        /// Categories added in the new version
        added: Vec<String>,
        /// Categories removed in the new version
        removed: Vec<String>,
    },
    /// The list of crate keywords has changed.
    KeywordsChange {
        /// Keywords added in the new version
        added: Vec<String>,
        /// Keywords removed in the new version
        removed: Vec<String>,
    },
    /// The crate repository URL has changed.
    RepositoryChange {
        /// Old repository URL
        old: Option<String>,
        /// New repository URL
        new: Option<String>,
    },
    /// The crate homepage URL has changed.
    HomepageChange {
        /// Old homepage URL
        old: Option<String>,
        /// New homepage URL
        new: Option<String>,
    },
    /// The crate documentation page URL has changed.
    DocumentationChange {
        /// Old documentation URL
        old: Option<String>,
        /// New documentation URL
        new: Option<String>,
    },
    /// The "native" library this crate links to has changed.
    LinksChange {
        /// Old linked native library
        old: Option<String>,
        /// New linked native library
        new: Option<String>,
    },
    /// The default run target of the crate has changed.
    DefaultRunChange {
        /// Old default run target
        old: Option<String>,
        /// New default run target
        new: Option<String>,
    },
    /// The additional crate metadata has changed.
    MetadataChange {
        /// Old additional metadata
        old: serde_json::Value,
        /// New additional metadata
        new: serde_json::Value,
    },

    // dependency changes
    /// A new dependency was added to the crate.
    DependencyAdded {
        /// Path of the dependency
        ///
        /// This includes the "target" (if present), dependecy kind (normal, "dev", or "build"), and
        /// the name of the dependency.
        path: String,
        /// Value of the dependency
        ///
        /// This includes information whether the crate is renamed on imported, the version
        /// requirement, whether the dependency is optional, whether the dependency uses
        /// default features, and the list of enabled crate features.
        value: String,
    },
    /// A dependency was removed from the crate.
    DependencyRemoved {
        /// Path of the dependency
        ///
        /// This includes the "target" (if present), dependecy kind (normal, "dev", or "build"), and
        /// the name of the dependency.
        path: String,
        /// Value of the dependency
        ///
        /// This includes information whether the crate is renamed on imported, the version
        /// requirement, whether the dependency is optional, whether the dependency uses
        /// default features, and the list of enabled crate features.
        value: String,
    },
    /// A dependency version has been upgraded.
    DependencyUpgraded {
        /// Path of the dependency
        ///
        /// This includes the "target" (if present), dependecy kind (normal, "dev", or "build"), and
        /// the name of the dependency.
        path: String,
        /// Old version requirement
        old: String,
        /// New version requirement
        new: String,
    },
    /// A dependency version has been downgraded.
    DependencyDowngraded {
        /// Path of the dependency
        ///
        /// This includes the "target" (if present), dependecy kind (normal, "dev", or "build"), and
        /// the name of the dependency.
        path: String,
        /// Old version requirement
        old: String,
        /// New version requirement
        new: String,
    },
    /// The feature flags of a dependency have changed.
    DependencyFeatures {
        /// Path of the dependency
        ///
        /// This includes the "target" (if present), dependecy kind (normal, "dev", or "build"), and
        /// the name of the dependency.
        path: String,
        /// Added feature flags
        added: Vec<String>,
        /// Removed feature flags
        removed: Vec<String>,
    },
    /// A dependency of this crate is now optional or is no longer optional.
    DependencyOptionality {
        /// Path of the dependency
        ///
        /// This includes the "target" (if present), dependecy kind (normal, "dev", or "build"), and
        /// the name of the dependency.
        path: String,
        /// Old optionality
        old: bool,
        /// New optionality
        new: bool,
    },

    // target changes
    /// A new compilation target was added to the crate.
    TargetAdded {
        /// Path of the target
        ///
        /// This includes the target "kind" and its name.
        path: String,
        /// Value of the target
        ///
        /// This includes the target path, edition, kinds (for library targets), required features,
        /// and flags whether the target contains doctests, tests, or documentation.
        target: String,
    },
    /// A compilation target was removed from the crate.
    TargetRemoved {
        /// Path of the target
        ///
        /// This includes the target "kind" and its name.
        path: String,
        /// Value of the target
        ///
        /// This includes the target path, edition, kinds (for library targets), required features,
        /// and flags whether the target contains doctests, tests, or documentation.
        target: String,
    },
    /// A compilation target has changed.
    TargetChanged {
        /// Path of the target
        ///
        /// This includes the target "kind" and its name.
        path: String,
        /// Old value of the target
        old: String,
        /// New value of the target
        new: String,
    },

    // feature changes
    /// A new feature flag was added to the crate.
    FeatureAdded {
        /// Name of the feature
        name: String,
    },
    /// A feature flag was removed from the crate.
    FeatureRemoved {
        /// Name of the feature
        name: String,
    },
    /// The dependencies of a feature flag have changed.
    FeatureChanged {
        /// Name of the feature
        name: String,
        /// List of added feature dependencies
        added: Vec<String>,
        /// List of removed feature dependencies
        removed: Vec<String>,
    },
}

#[derive(Serialize)]
struct JsonDiffItem {
    severity: String,
    kind: &'static str,
    data: Value,
}

impl DiffItem {
    /// Severity associated with this diff item.
    #[must_use]
    pub const fn severity(&self) -> Severity {
        #[expect(clippy::match_same_arms)]
        match self {
            Self::FileAdded { .. } => Severity::Info,
            Self::FileRemoved { .. } => Severity::Info,
            Self::FileChanged { .. } => Severity::Info,
            Self::LineEndingsChange { .. } => Severity::Warning,
            Self::PermissionChange { .. } => Severity::Warning,
            Self::NameChange { .. } => Severity::Error,
            Self::VersionChange { .. } => Severity::Info,
            Self::EditionChange { .. } => Severity::Warning,
            Self::RustVersionChange { .. } => Severity::Warning,
            Self::AuthorsChange { .. } => Severity::Warning,
            Self::DescriptionChange { .. } => Severity::Info,
            Self::LicenseChange { .. } => Severity::Warning,
            Self::LicenseFileChange { .. } => Severity::Warning,
            Self::ReadmeChange { .. } => Severity::Info,
            Self::CategoriesChange { .. } => Severity::Info,
            Self::KeywordsChange { .. } => Severity::Info,
            Self::RepositoryChange { .. } => Severity::Warning,
            Self::HomepageChange { .. } => Severity::Info,
            Self::DocumentationChange { .. } => Severity::Info,
            Self::LinksChange { .. } => Severity::Warning,
            Self::DefaultRunChange { .. } => Severity::Info,
            Self::MetadataChange { .. } => Severity::Info,
            Self::DependencyAdded { .. } => Severity::Info,
            Self::DependencyRemoved { .. } => Severity::Info,
            Self::DependencyUpgraded { .. } => Severity::Info,
            Self::DependencyDowngraded { .. } => Severity::Warning,
            Self::DependencyFeatures { .. } => Severity::Info,
            Self::DependencyOptionality { .. } => Severity::Info,
            Self::TargetAdded { .. } => Severity::Info,
            Self::TargetRemoved { .. } => Severity::Info,
            Self::TargetChanged { .. } => Severity::Info,
            Self::FeatureAdded { .. } => Severity::Info,
            Self::FeatureRemoved { .. } => Severity::Warning,
            Self::FeatureChanged { .. } => Severity::Info,
        }
    }

    /// String representation / ID of the diff item kind.
    #[must_use]
    // keep in sync with the Python enum
    pub const fn kind(&self) -> &'static str {
        match self {
            Self::FileAdded { .. } => "FileAdded",
            Self::FileRemoved { .. } => "FileRemoved",
            Self::FileChanged { .. } => "ContentChange",
            Self::LineEndingsChange { .. } => "LineEndingsChange",
            Self::PermissionChange { .. } => "PermissionChange",
            Self::NameChange { .. } => "NameChange",
            Self::VersionChange { .. } => "VersionChange",
            Self::EditionChange { .. } => "EditionChange",
            Self::RustVersionChange { .. } => "RustVersionChange",
            Self::AuthorsChange { .. } => "AuthorsChange",
            Self::DescriptionChange { .. } => "DescriptionChange",
            Self::LicenseChange { .. } => "LicenseChange",
            Self::LicenseFileChange { .. } => "LicenseFileChange",
            Self::ReadmeChange { .. } => "ReadmeChange",
            Self::CategoriesChange { .. } => "CategoriesChange",
            Self::KeywordsChange { .. } => "KeywordsChange",
            Self::RepositoryChange { .. } => "RepositoryChange",
            Self::HomepageChange { .. } => "HomepageChange",
            Self::DocumentationChange { .. } => "DocumentationChange",
            Self::LinksChange { .. } => "LinksChange",
            Self::DefaultRunChange { .. } => "DefaultRunChange",
            Self::MetadataChange { .. } => "MetadataChange",
            Self::DependencyAdded { .. } => "DependencyAdded",
            Self::DependencyRemoved { .. } => "DependencyRemoved",
            Self::DependencyUpgraded { .. } => "DependencyUpgraded",
            Self::DependencyDowngraded { .. } => "DependencyDowngraded",
            Self::DependencyFeatures { .. } => "DependencyFeatures",
            Self::DependencyOptionality { .. } => "DependencyOptionality",
            Self::TargetAdded { .. } => "TargetAdded",
            Self::TargetRemoved { .. } => "TargetRemoved",
            Self::TargetChanged { .. } => "TargetChanged",
            Self::FeatureAdded { .. } => "FeatureAdded",
            Self::FeatureRemoved { .. } => "FeatureRemoved",
            Self::FeatureChanged { .. } => "FeatureChanged",
        }
    }

    /// Data associated with this diff item.
    #[expect(clippy::too_many_lines)]
    #[must_use]
    pub fn data(&self) -> Value {
        let mut data = serde_json::Map::new();

        #[expect(clippy::match_same_arms)]
        match self {
            Self::FileAdded { path } => {
                data.insert(String::from("path"), Value::from(path.clone()));
            },
            Self::FileRemoved { path } => {
                data.insert(String::from("path"), Value::from(path.clone()));
            },
            Self::FileChanged { path, diff } => {
                data.insert(String::from("path"), Value::from(path.clone()));
                data.insert(String::from("diff"), Value::from(diff.clone()));
            },
            Self::LineEndingsChange { path } => {
                data.insert(String::from("path"), Value::from(Some(path.clone())));
            },
            Self::PermissionChange { path, old, new } => {
                data.insert(String::from("path"), Value::from(path.clone()));
                data.insert(String::from("old"), Value::from(old.clone()));
                data.insert(String::from("new"), Value::from(new.clone()));
            },
            Self::NameChange { old, new } => {
                data.insert(String::from("old"), Value::from(old.clone()));
                data.insert(String::from("new"), Value::from(new.clone()));
            },
            Self::VersionChange { old, new } => {
                data.insert(String::from("old"), Value::from(old.clone()));
                data.insert(String::from("new"), Value::from(new.clone()));
            },
            Self::EditionChange { old, new } => {
                data.insert(String::from("old"), Value::from(old.clone()));
                data.insert(String::from("new"), Value::from(new.clone()));
            },
            Self::RustVersionChange { old, new } => {
                data.insert(String::from("old"), Value::from(old.clone()));
                data.insert(String::from("new"), Value::from(new.clone()));
            },
            Self::AuthorsChange { added, removed } => {
                data.insert(String::from("added"), Value::from(added.clone()));
                data.insert(String::from("removed"), Value::from(removed.clone()));
            },
            Self::DescriptionChange { old, new } => {
                data.insert(String::from("old"), Value::from(old.clone()));
                data.insert(String::from("new"), Value::from(new.clone()));
            },
            Self::LicenseChange { old, new } => {
                data.insert(String::from("old"), Value::from(old.clone()));
                data.insert(String::from("new"), Value::from(new.clone()));
            },
            Self::LicenseFileChange { old, new } => {
                data.insert(String::from("old"), Value::from(old.clone()));
                data.insert(String::from("new"), Value::from(new.clone()));
            },
            Self::ReadmeChange { old, new } => {
                data.insert(String::from("old"), Value::from(old.clone()));
                data.insert(String::from("new"), Value::from(new.clone()));
            },
            Self::CategoriesChange { added, removed } => {
                data.insert(String::from("added"), Value::from(added.clone()));
                data.insert(String::from("removed"), Value::from(removed.clone()));
            },
            Self::KeywordsChange { added, removed } => {
                data.insert(String::from("added"), Value::from(added.clone()));
                data.insert(String::from("removed"), Value::from(removed.clone()));
            },
            Self::RepositoryChange { old, new } => {
                data.insert(String::from("old"), Value::from(old.clone()));
                data.insert(String::from("new"), Value::from(new.clone()));
            },
            Self::HomepageChange { old, new } => {
                data.insert(String::from("old"), Value::from(old.clone()));
                data.insert(String::from("new"), Value::from(new.clone()));
            },
            Self::DocumentationChange { old, new } => {
                data.insert(String::from("old"), Value::from(old.clone()));
                data.insert(String::from("new"), Value::from(new.clone()));
            },
            Self::LinksChange { old, new } => {
                data.insert(String::from("old"), Value::from(old.clone()));
                data.insert(String::from("new"), Value::from(new.clone()));
            },
            Self::DefaultRunChange { old, new } => {
                data.insert(String::from("old"), Value::from(old.clone()));
                data.insert(String::from("new"), Value::from(new.clone()));
            },
            Self::MetadataChange { old, new } => {
                data.insert(String::from("old"), old.clone());
                data.insert(String::from("new"), new.clone());
            },
            Self::DependencyAdded { path, value } => {
                data.insert(String::from("path"), Value::from(path.clone()));
                data.insert(String::from("value"), Value::from(value.clone()));
            },
            Self::DependencyRemoved { path, value } => {
                data.insert(String::from("path"), Value::from(path.clone()));
                data.insert(String::from("value"), Value::from(value.clone()));
            },
            Self::DependencyUpgraded { path, old, new } => {
                data.insert(String::from("path"), Value::from(path.clone()));
                data.insert(String::from("old"), Value::from(old.clone()));
                data.insert(String::from("new"), Value::from(new.clone()));
            },
            Self::DependencyDowngraded { path, old, new } => {
                data.insert(String::from("path"), Value::from(path.clone()));
                data.insert(String::from("old"), Value::from(old.clone()));
                data.insert(String::from("new"), Value::from(new.clone()));
            },
            Self::DependencyFeatures { path, removed, added } => {
                data.insert(String::from("path"), Value::from(path.clone()));
                data.insert(String::from("removed"), Value::from(removed.clone()));
                data.insert(String::from("added"), Value::from(added.clone()));
            },
            Self::DependencyOptionality { path, old, new } => {
                data.insert(String::from("path"), Value::from(path.clone()));
                data.insert(String::from("old"), Value::from(old.to_string()));
                data.insert(String::from("new"), Value::from(new.to_string()));
            },
            Self::TargetAdded { path, target } => {
                data.insert(String::from("path"), Value::from(path.clone()));
                data.insert(String::from("target"), Value::from(target.clone()));
            },
            Self::TargetRemoved { path, target } => {
                data.insert(String::from("path"), Value::from(path.clone()));
                data.insert(String::from("target"), Value::from(target.clone()));
            },
            Self::TargetChanged { path, old, new } => {
                data.insert(String::from("path"), Value::from(path.clone()));
                data.insert(String::from("old"), Value::from(old.clone()));
                data.insert(String::from("new"), Value::from(new.clone()));
            },
            Self::FeatureAdded { name } => {
                data.insert(String::from("name"), Value::from(name.clone()));
            },
            Self::FeatureRemoved { name } => {
                data.insert(String::from("name"), Value::from(name.clone()));
            },
            Self::FeatureChanged { name, removed, added } => {
                data.insert(String::from("name"), Value::from(name.clone()));
                data.insert(String::from("added"), Value::from(added.clone()));
                data.insert(String::from("removed"), Value::from(removed.clone()));
            },
        }

        Value::Object(data)
    }

    /// Human-readable message associated with this diff item.
    ///
    /// This message is limited to one line of text. If there is more information, it can be
    /// retrieved by calling [`DiffItem::extra`].
    #[must_use]
    pub fn message(&self) -> Cow<'static, str> {
        match self {
            Self::FileAdded { path } => format!("file added at path '{path}'").into(),
            Self::FileRemoved { path } => format!("file removed at path '{path}'").into(),
            Self::FileChanged { path, .. } => format!("file at path '{path}' changed").into(),
            Self::LineEndingsChange { path } => {
                format!("file at path '{path}' changed line endings (CRLF / LF)").into()
            },
            Self::PermissionChange { path, old, new } => {
                format!("file at path '{path}' changed mode from {old} to {new}").into()
            },
            Self::NameChange { old, new } => format!("crate name changed from '{old}' to '{new}'").into(),
            Self::VersionChange { old, new } => format!("crate version changed from '{old}' to '{new}'").into(),
            Self::EditionChange { old, new } => format!("crate edition changed from '{old}' to '{new}'").into(),
            Self::RustVersionChange { old, new } => {
                let old_msrv = format_option_string(old.as_ref());
                let new_msrv = format_option_string(new.as_ref());
                format!("crate MSRV changed from '{old_msrv}' to '{new_msrv}'").into()
            },
            Self::AuthorsChange { .. } => Into::into("crate authors changed"),
            Self::DescriptionChange { .. } => Into::into("crate description changed"),
            Self::LicenseChange { old, new } => {
                let old_license = format_option_string(old.as_ref());
                let new_license = format_option_string(new.as_ref());
                format!("crate license changed from '{old_license}' to '{new_license}'").into()
            },
            Self::LicenseFileChange { old, new } => {
                let old_path = format_option_string(old.as_ref());
                let new_path = format_option_string(new.as_ref());
                format!("crate license file changed from '{old_path}' to '{new_path}'").into()
            },
            Self::ReadmeChange { old, new } => {
                let old_path = format_option_string(old.as_ref());
                let new_path = format_option_string(new.as_ref());
                format!("crate readme file changed from '{old_path}' to '{new_path}'").into()
            },
            Self::CategoriesChange { .. } => Into::into("crate categories changed"),
            Self::KeywordsChange { .. } => Into::into("crate keywords changed"),
            Self::RepositoryChange { old, new } => {
                let old_url = format_option_string(old.as_ref());
                let new_url = format_option_string(new.as_ref());
                format!("crate repository URL file changed from '{old_url}' to '{new_url}'").into()
            },
            Self::HomepageChange { old, new } => {
                let old_url = format_option_string(old.as_ref());
                let new_url = format_option_string(new.as_ref());
                format!("crate homepage URL file changed from '{old_url}' to '{new_url}'").into()
            },
            Self::DocumentationChange { old, new } => {
                let old_url = format_option_string(old.as_ref());
                let new_url = format_option_string(new.as_ref());
                format!("crate documentation URL file changed from '{old_url}' to '{new_url}'").into()
            },
            Self::LinksChange { old, new } => {
                let old_link = format_option_string(old.as_ref());
                let new_link = format_option_string(new.as_ref());
                format!("linked library changed from '{old_link}' to '{new_link}'").into()
            },
            Self::DefaultRunChange { old, new } => {
                let old_target = format_option_string(old.as_ref());
                let new_target = format_option_string(new.as_ref());
                format!("default 'run' target changed from '{old_target}' to '{new_target}'").into()
            },
            Self::MetadataChange { .. } => Into::into("additional free-form crate metadata changed"),
            Self::DependencyAdded { path, .. } => format!("dependency '{path}' added").into(),
            Self::DependencyRemoved { path, .. } => format!("dependency '{path}' removed").into(),
            Self::DependencyUpgraded { path, old, new } => {
                format!("dependency '{path}' upgraded from {old} to {new}").into()
            },
            Self::DependencyDowngraded { path, old, new } => {
                format!("dependency '{path}' downgraded from {old} to {new}").into()
            },
            Self::DependencyFeatures { path, .. } => format!("dependency '{path}' features changed").into(),
            Self::DependencyOptionality { path, old, new } => {
                if !*old && *new {
                    format!("dependency '{path}' is now optional").into()
                } else {
                    // *old && !*new
                    format!("dependency '{path}' is no longer optional").into()
                }
            },
            Self::TargetAdded { path, .. } => format!("compilation target '{path}' added").into(),
            Self::TargetRemoved { path, .. } => format!("compilation target '{path}' removed").into(),
            Self::TargetChanged { path, .. } => format!("compilation target '{path}' changed").into(),
            Self::FeatureAdded { name } => format!("crate feature added: '{name}'").into(),
            Self::FeatureRemoved { name } => format!("crate feature removed: '{name}'").into(),
            Self::FeatureChanged { name, .. } => format!("crate feature '{name}' dependencies changed").into(),
        }
    }

    /// Additional message content from this report item.
    ///
    /// This contains more verbose information that might or might not span multiple lines.
    #[must_use]
    pub fn extra(&self) -> Option<String> {
        match self {
            Self::FileChanged { diff, .. } => diff.as_ref().map(ToOwned::to_owned),
            Self::AuthorsChange { added, removed }
            | Self::CategoriesChange { added, removed }
            | Self::KeywordsChange { added, removed }
            | Self::DependencyFeatures { added, removed, .. }
            | Self::FeatureChanged { added, removed, .. } => {
                let mut out = String::new();
                if !added.is_empty() {
                    let _ = writeln!(out, "added: {}", added.join(", "));
                }
                if !removed.is_empty() {
                    let _ = writeln!(out, "removed: {}", removed.join(", "));
                }
                Some(out)
            },
            Self::DescriptionChange { old, new } => {
                let old_desc = old.as_ref().map_or("", String::as_str);
                let new_desc = new.as_ref().map_or("", String::as_str);
                let header = Some(("old/package.description", "new/package.description"));
                Some(make_diff(old_desc, new_desc, header))
            },
            Self::MetadataChange { old, new } => {
                let mut out = String::new();
                let _ = writeln!(out, "old: {old}");
                let _ = writeln!(out, "new: {new}");
                Some(out)
            },
            Self::DependencyAdded { value, .. } | Self::DependencyRemoved { value, .. } => Some(value.clone()),
            Self::TargetAdded { target, .. } | Self::TargetRemoved { target, .. } => Some(target.clone()),
            Self::TargetChanged { old, new, .. } => {
                let mut out = String::new();
                let _ = writeln!(out, "old: {old}");
                let _ = writeln!(out, "new: {new}");
                Some(out)
            },
            _ => None,
        }
    }
}

impl Display for DiffItem {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        if f.alternate() {
            writeln!(f, "{}: {}", self.severity(), self.message())?;
            if let Some(extra) = self.extra() {
                for line in extra.lines() {
                    writeln!(f, "  {line}")?;
                }
                writeln!(f)?;
            }
            Ok(())
        } else {
            writeln!(f, "{}: {}", self.severity(), self.message())
        }
    }
}
