//! Tools for working with Debian control files.
use crate::editor::{Editor, EditorError, FsEditor, GeneratedFile};
use crate::relations::{ensure_relation, is_relation_implied};
use deb822_lossless::Paragraph;
use debian_control::lossless::relations::Relations;
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};

/// Format a description based on summary and long description lines.
pub fn format_description(summary: &str, long_description: Vec<&str>) -> String {
    let mut ret = summary.to_string() + "\n";
    for line in long_description {
        ret.push(' ');
        ret.push_str(line);
        ret.push('\n');
    }
    ret
}

#[derive(Debug, Clone, PartialEq, Eq, Copy)]
/// The type of a control file template.
pub enum TemplateType {
    /// A rule in the debian/rules file that generates the control file.
    Rules,

    /// Generated by gnome-pkg-tools.
    Gnome,

    /// Generated by pg_buildext.
    Postgresql,

    /// Generated by a set of files in the debian/control.in directory.
    Directory,

    /// Generated by cdbs.
    Cdbs,

    /// Generated by debcargo.
    Debcargo,
}

/// Error type for template expansion operations
#[derive(Debug)]
pub enum TemplateExpansionError {
    /// The expansion failed with an error message
    Failed(String),
    /// The expand command is missing
    ExpandCommandMissing(String),
    /// Unknown templating type encountered
    UnknownTemplating(PathBuf, Option<PathBuf>),
    /// A change conflict occurred
    Conflict(ChangeConflict),
}

impl From<ChangeConflict> for TemplateExpansionError {
    fn from(e: ChangeConflict) -> Self {
        TemplateExpansionError::Conflict(e)
    }
}

impl std::fmt::Display for TemplateExpansionError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            TemplateExpansionError::Failed(s) => write!(f, "Failed: {}", s),
            TemplateExpansionError::ExpandCommandMissing(s) => {
                write!(f, "Command not found: {}", s)
            }
            TemplateExpansionError::UnknownTemplating(p1, p2) => {
                if let Some(p2) = p2 {
                    write!(
                        f,
                        "Unknown templating: {} -> {}",
                        p1.display(),
                        p2.display()
                    )
                } else {
                    write!(f, "Unknown templating: {}", p1.display())
                }
            }
            TemplateExpansionError::Conflict(c) => write!(f, "Conflict: {}", c),
        }
    }
}

impl std::error::Error for TemplateExpansionError {}

/// Run the dh_gnome_clean command.
///
/// This needs to do some post-hoc cleaning, since dh_gnome_clean writes various debhelper log
/// files that should not be checked in.
///
/// # Arguments
/// * `path` - Path to run dh_gnome_clean in
///
/// # Errors
/// Returns an error if:
/// - Pre-existing .debhelper.log files are found
/// - No changelog file exists
/// - The dh_gnome_clean command is not found
/// - The command fails to execute
pub fn dh_gnome_clean(path: &std::path::Path) -> Result<(), TemplateExpansionError> {
    for entry in std::fs::read_dir(path.join("debian")).unwrap().flatten() {
        if entry
            .file_name()
            .to_string_lossy()
            .ends_with(".debhelper.log")
        {
            return Err(TemplateExpansionError::Failed(
                "pre-existing .debhelper.log files".to_string(),
            ));
        }
    }

    if !path.join("debian/changelog").exists() {
        return Err(TemplateExpansionError::Failed(
            "no changelog file".to_string(),
        ));
    }

    let result = std::process::Command::new("dh_gnome_clean")
        .current_dir(path)
        .output();

    match result {
        Ok(output) => {
            if !output.status.success() {
                let stderr = String::from_utf8_lossy(&output.stderr);
                return Err(TemplateExpansionError::Failed(stderr.to_string()));
            }
        }
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
            return Err(TemplateExpansionError::ExpandCommandMissing(
                "dh_gnome_clean".to_string(),
            ));
        }
        Err(e) => {
            return Err(TemplateExpansionError::Failed(e.to_string()));
        }
    }

    for entry in std::fs::read_dir(path.join("debian")).unwrap().flatten() {
        if entry
            .file_name()
            .to_string_lossy()
            .ends_with(".debhelper.log")
        {
            std::fs::remove_file(entry.path()).unwrap();
        }
    }

    Ok(())
}

/// Run the 'pg_buildext updatecontrol' command.
///
/// # Arguments
/// * `path` - Path to run pg_buildext updatecontrol in
///
/// # Errors
/// Returns an error if:
/// - The pg_buildext command is not found
/// - The command fails to execute
pub fn pg_buildext_updatecontrol(path: &std::path::Path) -> Result<(), TemplateExpansionError> {
    let result = std::process::Command::new("pg_buildext")
        .arg("updatecontrol")
        .current_dir(path)
        .output();

    match result {
        Ok(output) => {
            if !output.status.success() {
                let stderr = String::from_utf8_lossy(&output.stderr);
                return Err(TemplateExpansionError::Failed(stderr.to_string()));
            }
        }
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
            return Err(TemplateExpansionError::ExpandCommandMissing(
                "pg_buildext".to_string(),
            ));
        }
        Err(e) => {
            return Err(TemplateExpansionError::Failed(e.to_string()));
        }
    }
    Ok(())
}

/// Expand a control template.
///
/// # Arguments
/// * `template_path` - Path to the control template
/// * `path` - Path to the control file
/// * `template_type` - Type of the template
///
/// # Returns
/// Ok if the template was successfully expanded
fn expand_control_template(
    template_path: &std::path::Path,
    path: &std::path::Path,
    template_type: TemplateType,
) -> Result<(), TemplateExpansionError> {
    let package_root = path.parent().unwrap().parent().unwrap();
    match template_type {
        TemplateType::Rules => {
            let path_time = match std::fs::metadata(path) {
                Ok(metadata) => Some(metadata.modified().unwrap()),
                Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
                Err(e) => panic!("Failed to get mtime of {}: {}", path.display(), e),
            };
            while let Ok(metadata) = std::fs::metadata(template_path) {
                if Some(metadata.modified().unwrap()) == path_time {
                    // Wait until mtime has changed, so that make knows to regenerate.
                    filetime::set_file_mtime(template_path, filetime::FileTime::now()).unwrap();
                } else {
                    break;
                }
            }
            let result = std::process::Command::new("./debian/rules")
                .arg("debian/control")
                .current_dir(package_root)
                .output();

            match result {
                Ok(output) => {
                    if !output.status.success() {
                        let stderr = String::from_utf8_lossy(&output.stderr);
                        Err(TemplateExpansionError::Failed(format!(
                            "Exit code {} running ./debian/rules debian/control: {}",
                            output.status, stderr
                        )))
                    } else {
                        Ok(())
                    }
                }
                Err(e) => Err(TemplateExpansionError::Failed(format!(
                    "Failed to run ./debian/rules debian/control: {}",
                    e
                ))),
            }
        }
        TemplateType::Gnome => dh_gnome_clean(package_root),
        TemplateType::Postgresql => pg_buildext_updatecontrol(package_root),
        TemplateType::Cdbs => unreachable!(),
        TemplateType::Debcargo => unreachable!(),
        TemplateType::Directory => Err(TemplateExpansionError::UnknownTemplating(
            path.to_path_buf(),
            Some(template_path.to_path_buf()),
        )),
    }
}

#[derive(Debug, Clone)]
struct Deb822Changes(
    std::collections::HashMap<(String, String), Vec<(String, Option<String>, Option<String>)>>,
);

impl Deb822Changes {
    fn new() -> Self {
        Self(std::collections::HashMap::new())
    }

    fn insert(
        &mut self,
        para_key: (String, String),
        field: String,
        old_value: Option<String>,
        new_value: Option<String>,
    ) {
        self.0
            .entry(para_key)
            .or_default()
            .push((field, old_value, new_value));
    }

    #[allow(dead_code)]
    fn normalized(&self) -> Vec<((&str, &str), Vec<(&str, Option<&str>, Option<&str>)>)> {
        let mut ret: Vec<_> = self
            .0
            .iter()
            .map(|(k, v)| {
                ((k.0.as_str(), k.1.as_str()), {
                    let mut v: Vec<_> = v
                        .iter()
                        .map(|(f, o, n)| (f.as_str(), o.as_deref(), n.as_deref()))
                        .collect();
                    v.sort();
                    v
                })
            })
            .collect();
        ret.sort_by_key(|(k, _)| *k);
        ret
    }
}

// Update a control file template based on changes to the file itself.
//
// # Arguments
// * `template_path` - Path to the control template
// * `path` - Path to the control file
// * `changes` - Changes to apply
// * `expand_template` - Whether to expand the template after updating it
//
// # Returns
// Ok if the template was successfully updated
fn update_control_template(
    template_path: &std::path::Path,
    template_type: TemplateType,
    path: &std::path::Path,
    changes: Deb822Changes,
    expand_template: bool,
) -> Result<bool, TemplateExpansionError> {
    if template_type == TemplateType::Directory {
        // We can't handle these yet
        return Err(TemplateExpansionError::UnknownTemplating(
            path.to_path_buf(),
            Some(template_path.to_path_buf()),
        ));
    }

    let mut template_editor =
        FsEditor::<deb822_lossless::Deb822>::new(template_path, true, false).unwrap();

    let resolve_conflict = match template_type {
        TemplateType::Cdbs => Some(resolve_cdbs_template as ResolveDeb822Conflict),
        _ => None,
    };

    apply_changes(&mut template_editor, changes.clone(), resolve_conflict)?;

    if !template_editor.has_changed() {
        // A bit odd, since there were changes to the output file. Anyway.
        return Ok(false);
    }

    match template_editor.commit() {
        Ok(_) => {}
        Err(e) => return Err(TemplateExpansionError::Failed(e.to_string())),
    }

    if expand_template {
        match template_type {
            TemplateType::Cdbs => {
                let mut editor =
                    FsEditor::<deb822_lossless::Deb822>::new(path, true, false).unwrap();
                apply_changes(&mut editor, changes, None)?;
                match editor.commit() {
                    Ok(_) => {}
                    Err(e) => return Err(TemplateExpansionError::Failed(e.to_string())),
                }
            }
            _ => {
                expand_control_template(template_path, path, template_type)?;
            }
        }
    }

    Ok(true)
}

#[derive(Debug, PartialEq, Eq)]
/// A change conflict.
pub struct ChangeConflict {
    /// Paragraph key, i.e. ("Source", "foo")
    pub para_key: (String, String),
    /// Field that conflicted
    pub field: String,
    /// Old value in the control file
    pub actual_old_value: Option<String>,
    /// Old value in the template
    pub template_old_value: Option<String>,
    /// New value in the control file
    pub actual_new_value: Option<String>,
}

impl std::fmt::Display for ChangeConflict {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(
            f,
            "{}/{}: {} -> {} (template: {})",
            self.para_key.0,
            self.para_key.1,
            self.actual_old_value.as_deref().unwrap_or(""),
            self.actual_new_value.as_deref().unwrap_or(""),
            self.template_old_value.as_deref().unwrap_or("")
        )
    }
}

impl std::error::Error for ChangeConflict {}

type ResolveDeb822Conflict = fn(
    para_key: (&str, &str),
    field: &str,
    actual_old_value: Option<&str>,
    template_old_value: Option<&str>,
    actual_new_value: Option<&str>,
) -> Result<Option<String>, ChangeConflict>;

fn resolve_cdbs_template(
    para_key: (&str, &str),
    field: &str,
    actual_old_value: Option<&str>,
    template_old_value: Option<&str>,
    actual_new_value: Option<&str>,
) -> Result<Option<String>, ChangeConflict> {
    if para_key.0 == "Source"
        && field == "Build-Depends"
        && template_old_value.is_some()
        && actual_old_value.is_some()
        && actual_new_value.is_some()
    {
        if actual_new_value
            .unwrap()
            .contains(actual_old_value.unwrap())
        {
            // We're simply adding to the existing list
            return Ok(Some(
                actual_new_value
                    .unwrap()
                    .replace(actual_old_value.unwrap(), template_old_value.unwrap()),
            ));
        } else {
            let old_rels: Relations = actual_old_value.unwrap().parse().unwrap();
            let new_rels: Relations = actual_new_value.unwrap().parse().unwrap();
            let template_old_value = template_old_value.unwrap();
            let (mut ret, errors) = Relations::parse_relaxed(template_old_value, true);
            if !errors.is_empty() {
                log::debug!("Errors parsing template value: {:?}", errors);
            }
            for v in new_rels.entries() {
                if old_rels.entries().any(|r| is_relation_implied(&v, &r)) {
                    continue;
                }
                ensure_relation(&mut ret, v);
            }
            return Ok(Some(ret.to_string()));
        }
    }
    Err(ChangeConflict {
        para_key: (para_key.0.to_string(), para_key.1.to_string()),
        field: field.to_string(),
        actual_old_value: actual_old_value.map(|v| v.to_string()),
        template_old_value: template_old_value.map(|v| v.to_string()),
        actual_new_value: actual_new_value.map(|s| s.to_string()),
    })
}

/// Guess the type for a control template.
///
/// # Arguments
/// * `template_path` - Path to the control template
/// * `debian_path` - Path to the debian directory
///
/// # Returns
/// Template type; None if unknown
pub fn guess_template_type(
    template_path: &std::path::Path,
    debian_path: Option<&std::path::Path>,
) -> Option<TemplateType> {
    // TODO(jelmer): This should use a proper make file parser of some sort..
    if let Some(debian_path) = debian_path {
        match std::fs::read(debian_path.join("rules")) {
            Ok(file) => {
                for line in file.split(|&c| c == b'\n') {
                    if line.starts_with(b"debian/control:") {
                        return Some(TemplateType::Rules);
                    }
                    if line.starts_with(b"debian/%: debian/%.in") {
                        return Some(TemplateType::Rules);
                    }
                    if line.starts_with(b"include /usr/share/blends-dev/rules") {
                        return Some(TemplateType::Rules);
                    }
                }
            }
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
            Err(e) => panic!(
                "Failed to read {}: {}",
                debian_path.join("rules").display(),
                e
            ),
        }
    }
    match std::fs::read(template_path) {
        Ok(template) => {
            let template_str = std::str::from_utf8(&template).unwrap();
            if template_str.contains("@GNOME_TEAM@") {
                return Some(TemplateType::Gnome);
            }
            if template_str.contains("PGVERSION") {
                return Some(TemplateType::Postgresql);
            }
            if template_str.contains("@cdbs@") {
                return Some(TemplateType::Cdbs);
            }

            let control = debian_control::Control::read_relaxed(std::io::Cursor::new(&template))
                .unwrap()
                .0;

            let build_depends = control.source().and_then(|s| s.build_depends());

            if build_depends.iter().any(|d| {
                d.entries()
                    .any(|e| e.relations().any(|r| r.name() == "gnome-pkg-tools"))
            }) {
                return Some(TemplateType::Gnome);
            }

            if build_depends.iter().any(|d| {
                d.entries()
                    .any(|e| e.relations().any(|r| r.name() == "cdbs"))
            }) {
                return Some(TemplateType::Cdbs);
            }
        }
        Err(_) if template_path.is_dir() => {
            return Some(TemplateType::Directory);
        }
        Err(e) => panic!("Failed to read {}: {}", template_path.display(), e),
    }
    if let Some(debian_path) = debian_path {
        if debian_path.join("debcargo.toml").exists() {
            return Some(TemplateType::Debcargo);
        }
    }
    None
}

/// Apply a set of changes to this deb822 instance.
///
/// # Arguments
/// * `changes` - Changes to apply
/// * `resolve_conflict` - Callback to resolve conflicts
fn apply_changes(
    deb822: &mut deb822_lossless::Deb822,
    mut changes: Deb822Changes,
    resolve_conflict: Option<ResolveDeb822Conflict>,
) -> Result<(), ChangeConflict> {
    fn default_resolve_conflict(
        para_key: (&str, &str),
        field: &str,
        actual_old_value: Option<&str>,
        template_old_value: Option<&str>,
        actual_new_value: Option<&str>,
    ) -> Result<Option<String>, ChangeConflict> {
        Err(ChangeConflict {
            para_key: (para_key.0.to_string(), para_key.1.to_string()),
            field: field.to_string(),
            actual_old_value: actual_old_value.map(|v| v.to_string()),
            template_old_value: template_old_value.map(|v| v.to_string()),
            actual_new_value: actual_new_value.map(|s| s.to_string()),
        })
    }

    let resolve_conflict = resolve_conflict.unwrap_or(default_resolve_conflict);

    for mut paragraph in deb822.paragraphs() {
        let items: Vec<_> = paragraph.items().collect();
        for item in items {
            for (key, old_value, mut new_value) in changes.0.remove(&item).unwrap_or_default() {
                if paragraph.get(&key) != old_value {
                    new_value = resolve_conflict(
                        (&item.0, &item.1),
                        &key,
                        old_value.as_deref(),
                        paragraph.get(&key).as_deref(),
                        new_value.as_deref(),
                    )?;
                }
                if let Some(new_value) = new_value.as_ref() {
                    paragraph.set(&key, new_value);
                } else {
                    paragraph.remove(&key);
                }
            }
        }
    }
    // Add any new paragraphs that weren't processed earlier
    for (key, p) in changes.0.drain() {
        let mut paragraph = deb822.add_paragraph();
        for (field, old_value, mut new_value) in p {
            if old_value.is_some() {
                new_value = resolve_conflict(
                    (&key.0, &key.1),
                    &field,
                    old_value.as_deref(),
                    paragraph.get(&field).as_deref(),
                    new_value.as_deref(),
                )?;
            }
            if let Some(new_value) = new_value {
                paragraph.set(&field, &new_value);
            }
        }
    }
    Ok(())
}

fn find_template_path(path: &Path) -> Option<PathBuf> {
    for ext in &["in", "m4"] {
        let template_path = path.with_extension(ext);
        if template_path.exists() {
            return Some(template_path);
        }
    }
    None
}

/// An editor for a control file that may be generated from a template.
///
/// This editor will automatically expand the template if it does not exist.
/// It will also automatically update the template if the control file is changed.
///
/// # Example
///
/// ```rust
/// use std::path::Path;
/// use debian_workbench::control::TemplatedControlEditor;
/// let td = tempfile::tempdir().unwrap();
/// let mut editor = TemplatedControlEditor::create(td.path().join("control")).unwrap();
/// editor.add_source("foo").set_architecture(Some("all"));
/// editor.commit().unwrap();
/// ```
pub struct TemplatedControlEditor {
    /// The primary editor for the control file.
    primary: FsEditor<debian_control::Control>,
    /// The template that was used to generate the control file.
    template: Option<Template>,
    /// Path to the control file.
    path: PathBuf,
    /// Whether the control file itself should not be written to disk.
    template_only: bool,
}

impl Deref for TemplatedControlEditor {
    type Target = debian_control::Control;

    fn deref(&self) -> &Self::Target {
        &self.primary
    }
}

impl DerefMut for TemplatedControlEditor {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.primary
    }
}

impl TemplatedControlEditor {
    /// Create a new control file editor.
    pub fn create<P: AsRef<Path>>(
        control_path: P,
    ) -> Result<Self, EditorError<deb822_lossless::Error>> {
        if control_path.as_ref().exists() {
            return Err(EditorError::IoError(std::io::Error::new(
                std::io::ErrorKind::AlreadyExists,
                "Control file already exists",
            )));
        }
        Self::new(control_path, true)
    }

    /// Return the type of the template used to generate the control file.
    pub fn template_type(&self) -> Option<TemplateType> {
        self.template.as_ref().map(|t| t.template_type)
    }

    /// Normalize field spacing in the control file.
    ///
    /// For template-based control files with deb822-style templates (CDBS, Directory),
    /// this will normalize both the template file and the primary control file.
    /// For non-deb822 templates (Rules, Gnome, Postgresql, etc.), this returns an error
    /// since those control files are generated and shouldn't be normalized.
    /// For files without templates, it normalizes the control file directly.
    ///
    /// # Returns
    /// An error if a template exists but cannot be normalized, or if the template
    /// is not a deb822-style template.
    pub fn normalize_field_spacing(&mut self) -> Result<(), EditorError<deb822_lossless::Error>> {
        let Some(template) = &self.template else {
            // No template: normalize the control file directly
            self.primary.as_mut_deb822().normalize_field_spacing();
            return Ok(());
        };

        // Check if this is a deb822-style template
        let is_deb822_template = matches!(
            template.template_type,
            TemplateType::Cdbs | TemplateType::Directory
        );

        if !is_deb822_template {
            // Non-deb822 template: cannot normalize generated control files
            return Err(EditorError::GeneratedFile(
                self.path.clone(),
                GeneratedFile {
                    template_path: Some(template.template_path.clone()),
                    template_type: None,
                },
            ));
        }

        // For deb822-style templates: normalize the template file
        let mut template_editor =
            FsEditor::<deb822_lossless::Deb822>::new(&template.template_path, true, false)?;
        template_editor.normalize_field_spacing();
        template_editor.commit()?;

        // Also normalize the primary control file
        self.primary.as_mut_deb822().normalize_field_spacing();
        Ok(())
    }

    /// Open an existing control file.
    pub fn open<P: AsRef<Path>>(
        control_path: P,
    ) -> Result<Self, EditorError<deb822_lossless::Error>> {
        Self::new(control_path, false)
    }

    /// Create a new control file editor.
    pub fn new<P: AsRef<Path>>(
        control_path: P,
        allow_missing: bool,
    ) -> Result<Self, EditorError<deb822_lossless::Error>> {
        let path = control_path.as_ref();
        let (template, template_only) = if !path.exists() {
            if let Some(template) = Template::find(path) {
                match template.expand() {
                    Ok(_) => {}
                    Err(e) => {
                        return Err(EditorError::TemplateError(
                            template.template_path,
                            e.to_string(),
                        ))
                    }
                }
                (Some(template), true)
            } else if !allow_missing {
                return Err(EditorError::IoError(std::io::Error::new(
                    std::io::ErrorKind::NotFound,
                    "No control file or template found",
                )));
            } else {
                (None, false)
            }
        } else {
            (Template::find(path), false)
        };
        let primary = FsEditor::<debian_control::Control>::new(path, false, false)?;
        Ok(Self {
            path: path.to_path_buf(),
            primary,
            template_only,
            template,
        })
    }

    /// Return a dictionary describing the changes since the base.
    ///
    /// # Returns
    /// A dictionary mapping tuples of (kind, name) to list of (field_name, old_value, new_value)
    fn changes(&self) -> Deb822Changes {
        let orig = deb822_lossless::Deb822::read_relaxed(self.primary.orig_content().unwrap())
            .unwrap()
            .0;
        let mut changes = Deb822Changes::new();

        fn by_key(
            ps: impl Iterator<Item = Paragraph>,
        ) -> std::collections::HashMap<(String, String), Paragraph> {
            let mut ret = std::collections::HashMap::new();
            for p in ps {
                if let Some(s) = p.get("Source") {
                    ret.insert(("Source".to_string(), s), p);
                } else if let Some(s) = p.get("Package") {
                    ret.insert(("Package".to_string(), s), p);
                } else {
                    let k = p.items().next().unwrap();
                    ret.insert(k, p);
                }
            }
            ret
        }

        let orig_by_key = by_key(orig.paragraphs());
        let new_by_key = by_key(self.as_deb822().paragraphs());
        let keys = orig_by_key
            .keys()
            .chain(new_by_key.keys())
            .collect::<std::collections::HashSet<_>>();
        for key in keys {
            let old = orig_by_key.get(key);
            let new = new_by_key.get(key);
            if old == new {
                continue;
            }
            let fields = std::collections::HashSet::<String>::from_iter(
                old.iter()
                    .flat_map(|p| p.keys())
                    .chain(new.iter().flat_map(|p| p.keys())),
            );
            for field in &fields {
                let old_val = old.and_then(|x| x.get(field));
                let new_val = new.and_then(|x| x.get(field));
                if old_val != new_val {
                    changes.insert(key.clone(), field.to_string(), old_val, new_val);
                }
            }
        }
        changes
    }

    /// Commit the changes to the control file and template.
    pub fn commit(&mut self) -> Result<Vec<PathBuf>, EditorError<deb822_lossless::Error>> {
        let mut changed_files: Vec<PathBuf> = vec![];
        if self.template_only {
            // Remove the control file if it exists.
            match std::fs::remove_file(self.path.as_path()) {
                Ok(_) => {}
                Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
                Err(e) => return Err(EditorError::IoError(e)),
            }

            changed_files.push(self.path.clone());

            let template = self
                .template
                .as_ref()
                .expect("template_only implies template");

            // Update the template
            let changed = match template.update(self.changes(), false) {
                Ok(changed) => changed,
                Err(e) => {
                    return Err(EditorError::TemplateError(
                        template.template_path.clone(),
                        e.to_string(),
                    ))
                }
            };
            if changed {
                changed_files.push(template.template_path.clone());
            }
        } else {
            match self.primary.commit() {
                Ok(files) => {
                    changed_files.extend(files.iter().map(|p| p.to_path_buf()));
                }
                Err(EditorError::GeneratedFile(
                    p,
                    GeneratedFile {
                        template_path: tp,
                        template_type: tt,
                    },
                )) => {
                    if tp.is_none() {
                        return Err(EditorError::GeneratedFile(
                            p,
                            GeneratedFile {
                                template_path: tp,
                                template_type: tt,
                            },
                        ));
                    }
                    let template = if let Some(template) = self.template.as_ref() {
                        template
                    } else {
                        return Err(EditorError::IoError(std::io::Error::new(
                            std::io::ErrorKind::NotFound,
                            "No control file or template found",
                        )));
                    };
                    let changes = self.changes();
                    let changed = match template.update(changes, true) {
                        Ok(changed) => changed,
                        Err(e) => {
                            return Err(EditorError::TemplateError(tp.unwrap(), e.to_string()))
                        }
                    };
                    changed_files = if changed {
                        vec![tp.as_ref().unwrap().to_path_buf(), p]
                    } else {
                        vec![]
                    };
                }
                Err(EditorError::IoError(e)) if e.kind() == std::io::ErrorKind::NotFound => {
                    let template = if let Some(p) = self.template.as_ref() {
                        p
                    } else {
                        return Err(EditorError::IoError(std::io::Error::new(
                            std::io::ErrorKind::NotFound,
                            "No control file or template found",
                        )));
                    };
                    let changed = match template.update(self.changes(), !self.template_only) {
                        Ok(changed) => changed,
                        Err(e) => {
                            return Err(EditorError::TemplateError(
                                template.template_path.clone(),
                                e.to_string(),
                            ))
                        }
                    };
                    if changed {
                        changed_files.push(template.template_path.clone());
                        changed_files.push(self.path.clone());
                    }
                }
                Err(e) => return Err(e),
            }
        }

        Ok(changed_files)
    }
}

struct Template {
    path: PathBuf,
    template_path: PathBuf,
    template_type: TemplateType,
}

impl Template {
    fn find(path: &Path) -> Option<Self> {
        let template_path = find_template_path(path)?;
        let template_type = guess_template_type(&template_path, Some(path.parent().unwrap()))?;
        Some(Self {
            path: path.to_path_buf(),
            template_path,
            template_type,
        })
    }

    fn expand(&self) -> Result<(), TemplateExpansionError> {
        expand_control_template(&self.template_path, &self.path, self.template_type)
    }

    fn update(&self, changes: Deb822Changes, expand: bool) -> Result<bool, TemplateExpansionError> {
        update_control_template(
            &self.template_path,
            self.template_type,
            &self.path,
            changes,
            expand,
        )
    }
}

impl Editor<debian_control::Control> for TemplatedControlEditor {
    fn orig_content(&self) -> Option<&[u8]> {
        self.primary.orig_content()
    }

    fn updated_content(&self) -> Option<Vec<u8>> {
        self.primary.updated_content()
    }

    fn rewritten_content(&self) -> Option<&[u8]> {
        self.primary.rewritten_content()
    }

    fn is_generated(&self) -> bool {
        self.primary.is_generated()
    }

    fn commit(&mut self) -> Result<Vec<std::path::PathBuf>, EditorError<deb822_lossless::Error>> {
        TemplatedControlEditor::commit(self)
    }

    fn revert(&mut self) -> Result<(), EditorError<deb822_lossless::Error>> {
        self.primary.revert()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_format_description() {
        let summary = "Summary";
        let long_description = vec!["Long", "Description"];
        let expected = "Summary\n Long\n Description\n";
        assert_eq!(format_description(summary, long_description), expected);
    }

    #[test]
    fn test_resolve_cdbs_conflicts() {
        let val = resolve_cdbs_template(
            ("Source", "libnetsds-perl"),
            "Build-Depends",
            Some("debhelper (>= 6), foo"),
            Some("@cdbs@, debhelper (>= 9)"),
            Some("debhelper (>= 10), foo"),
        )
        .unwrap();

        assert_eq!(val, Some("@cdbs@, debhelper (>= 10)".to_string()));

        let val = resolve_cdbs_template(
            ("Source", "libnetsds-perl"),
            "Build-Depends",
            Some("debhelper (>= 6), foo"),
            Some("@cdbs@, foo"),
            Some("debhelper (>= 10), foo"),
        )
        .unwrap();
        assert_eq!(val, Some("@cdbs@, debhelper (>= 10), foo".to_string()));
        let val = resolve_cdbs_template(
            ("Source", "libnetsds-perl"),
            "Build-Depends",
            Some("debhelper (>= 6), foo"),
            Some("@cdbs@, debhelper (>= 9)"),
            Some("debhelper (>= 10), foo"),
        )
        .unwrap();
        assert_eq!(val, Some("@cdbs@, debhelper (>= 10)".to_string()));
    }

    #[test]
    fn test_normalize_field_spacing_without_template() {
        // Test normalize_field_spacing on a control file without a template
        let td = tempfile::tempdir().unwrap();
        let control_path = td.path().join("debian").join("control");
        std::fs::create_dir_all(control_path.parent().unwrap()).unwrap();
        std::fs::write(
            &control_path,
            b"Source: test\nRecommends:  foo\n\nPackage: test\nArchitecture: all\n",
        )
        .unwrap();

        let mut editor = TemplatedControlEditor::open(&control_path).unwrap();

        // Check the original spacing
        let original = editor.as_deb822().to_string();
        assert_eq!(
            original,
            "Source: test\nRecommends:  foo\n\nPackage: test\nArchitecture: all\n"
        );

        // Normalize field spacing using the new method
        editor.normalize_field_spacing().unwrap();

        // Check if normalization worked
        let normalized = editor.as_deb822().to_string();
        assert_eq!(
            normalized,
            "Source: test\nRecommends: foo\n\nPackage: test\nArchitecture: all\n"
        );
    }

    #[test]
    fn test_normalize_field_spacing_with_non_deb822_template() {
        // Test that normalize_field_spacing returns an error for non-deb822 templates
        let td = tempfile::tempdir().unwrap();
        let debian_path = td.path().join("debian");
        std::fs::create_dir_all(&debian_path).unwrap();

        let control_in_path = debian_path.join("control.in");
        let control_path = debian_path.join("control");
        let rules_path = debian_path.join("rules");

        // Create a Rules-style template
        std::fs::write(
            &control_in_path,
            b"Source: test\nRecommends:  foo\n\nPackage: test\nArchitecture: all\n",
        )
        .unwrap();

        // Create a rules file that generates control
        std::fs::write(
            &rules_path,
            b"#!/usr/bin/make -f\n\ndebian/control: debian/control.in\n\tcp $< $@\n",
        )
        .unwrap();

        // Create a generated control file
        std::fs::write(
            &control_path,
            b"Source: test\nRecommends:  foo\n\nPackage: test\nArchitecture: all\n",
        )
        .unwrap();

        let mut editor = TemplatedControlEditor::open(&control_path).unwrap();
        assert_eq!(editor.template_type(), Some(TemplateType::Rules));

        // Normalize field spacing should fail for non-deb822 templates
        let result = editor.normalize_field_spacing();
        assert!(result.is_err());

        // Verify it's a GeneratedFile error
        match result {
            Err(EditorError::GeneratedFile(_, _)) => {
                // Expected error type
            }
            _ => panic!("Expected GeneratedFile error"),
        }
    }

    #[test]
    fn test_normalize_field_spacing_with_cdbs_template() {
        // Test normalize_field_spacing with a CDBS template
        // Note: We can't actually expand CDBS templates in tests, so we test that
        // the template file itself gets normalized
        let td = tempfile::tempdir().unwrap();
        let debian_path = td.path().join("debian");
        std::fs::create_dir_all(&debian_path).unwrap();

        let control_in_path = debian_path.join("control.in");
        let control_path = debian_path.join("control");

        // Create a CDBS-style template with double spacing
        std::fs::write(
            &control_in_path,
            b"Source: test\nBuild-Depends:  @cdbs@\nRecommends:  ${cdbs:Recommends}\n\nPackage: test\nArchitecture: all\n",
        )
        .unwrap();

        // Create a control file (pretending it was generated from the template)
        std::fs::write(
            &control_path,
            b"Source: test\nBuild-Depends: debhelper\nRecommends:  foo\n\nPackage: test\nArchitecture: all\n",
        )
        .unwrap();

        let mut editor = TemplatedControlEditor::open(&control_path).unwrap();
        assert_eq!(editor.template_type(), Some(TemplateType::Cdbs));

        // Verify original template has double spacing
        let original_template = std::fs::read_to_string(&control_in_path).unwrap();
        assert_eq!(
            original_template,
            "Source: test\nBuild-Depends:  @cdbs@\nRecommends:  ${cdbs:Recommends}\n\nPackage: test\nArchitecture: all\n"
        );

        // Normalize field spacing - this should normalize both template and primary
        editor.normalize_field_spacing().unwrap();

        // Check that the template was normalized
        let template_content = std::fs::read_to_string(&control_in_path).unwrap();
        assert_eq!(
            template_content,
            "Source: test\nBuild-Depends: @cdbs@\nRecommends: ${cdbs:Recommends}\n\nPackage: test\nArchitecture: all\n"
        );

        // Check that the primary control file was also normalized
        let control_content = editor.as_deb822().to_string();
        assert_eq!(
            control_content,
            "Source: test\nBuild-Depends: debhelper\nRecommends: foo\n\nPackage: test\nArchitecture: all\n"
        );
    }

    mod guess_template_type {

        #[test]
        fn test_rules_generates_control() {
            let td = tempfile::tempdir().unwrap();
            std::fs::create_dir(td.path().join("debian")).unwrap();
            std::fs::write(
                td.path().join("debian/rules"),
                r#"%:
	dh $@

debian/control: debian/control.in
	cp $@ $<
"#,
            )
            .unwrap();
            assert_eq!(
                super::guess_template_type(
                    &td.path().join("debian/control.in"),
                    Some(&td.path().join("debian"))
                ),
                Some(super::TemplateType::Rules)
            );
        }

        #[test]
        fn test_rules_generates_control_percent() {
            let td = tempfile::tempdir().unwrap();
            std::fs::create_dir(td.path().join("debian")).unwrap();
            std::fs::write(
                td.path().join("debian/rules"),
                r#"%:
	dh $@

debian/%: debian/%.in
	cp $@ $<
"#,
            )
            .unwrap();
            assert_eq!(
                super::guess_template_type(
                    &td.path().join("debian/control.in"),
                    Some(&td.path().join("debian"))
                ),
                Some(super::TemplateType::Rules)
            );
        }

        #[test]
        fn test_rules_generates_control_blends() {
            let td = tempfile::tempdir().unwrap();
            std::fs::create_dir(td.path().join("debian")).unwrap();
            std::fs::write(
                td.path().join("debian/rules"),
                r#"%:
	dh $@

include /usr/share/blends-dev/rules
"#,
            )
            .unwrap();
            assert_eq!(
                super::guess_template_type(
                    &td.path().join("debian/control.stub"),
                    Some(&td.path().join("debian"))
                ),
                Some(super::TemplateType::Rules)
            );
        }

        #[test]
        fn test_empty_template() {
            let td = tempfile::tempdir().unwrap();
            std::fs::create_dir(td.path().join("debian")).unwrap();
            // No paragraph
            std::fs::write(td.path().join("debian/control.in"), "").unwrap();

            assert_eq!(
                None,
                super::guess_template_type(
                    &td.path().join("debian/control.in"),
                    Some(&td.path().join("debian"))
                )
            );
        }

        #[test]
        fn test_build_depends_cdbs() {
            let td = tempfile::tempdir().unwrap();
            std::fs::create_dir(td.path().join("debian")).unwrap();
            std::fs::write(
                td.path().join("debian/control.in"),
                r#"Source: blah
Build-Depends: cdbs
Vcs-Git: file://

Package: bar
"#,
            )
            .unwrap();
            assert_eq!(
                Some(super::TemplateType::Cdbs),
                super::guess_template_type(
                    &td.path().join("debian/control.in"),
                    Some(&td.path().join("debian"))
                )
            );
        }

        #[test]
        fn test_no_build_depends() {
            let td = tempfile::tempdir().unwrap();
            std::fs::create_dir(td.path().join("debian")).unwrap();
            std::fs::write(
                td.path().join("debian/control.in"),
                r#"Source: blah
Vcs-Git: file://

Package: bar
"#,
            )
            .unwrap();
            assert_eq!(
                None,
                super::guess_template_type(
                    &td.path().join("debian/control.in"),
                    Some(&td.path().join("debian"))
                )
            );
        }

        #[test]
        fn test_gnome() {
            let td = tempfile::tempdir().unwrap();
            std::fs::create_dir(td.path().join("debian")).unwrap();
            std::fs::write(
                td.path().join("debian/control.in"),
                r#"Foo @GNOME_TEAM@
"#,
            )
            .unwrap();
            assert_eq!(
                Some(super::TemplateType::Gnome),
                super::guess_template_type(
                    &td.path().join("debian/control.in"),
                    Some(&td.path().join("debian"))
                )
            );
        }

        #[test]
        fn test_gnome_build_depends() {
            let td = tempfile::tempdir().unwrap();
            std::fs::create_dir(td.path().join("debian")).unwrap();
            std::fs::write(
                td.path().join("debian/control.in"),
                r#"Source: blah
Build-Depends: gnome-pkg-tools, libc6-dev
"#,
            )
            .unwrap();
            assert_eq!(
                Some(super::TemplateType::Gnome),
                super::guess_template_type(
                    &td.path().join("debian/control.in"),
                    Some(&td.path().join("debian"))
                )
            );
        }

        #[test]
        fn test_cdbs() {
            let td = tempfile::tempdir().unwrap();
            std::fs::create_dir(td.path().join("debian")).unwrap();
            std::fs::write(
                td.path().join("debian/control.in"),
                r#"Source: blah
Build-Depends: debhelper, cdbs
"#,
            )
            .unwrap();
            assert_eq!(
                Some(super::TemplateType::Cdbs),
                super::guess_template_type(
                    &td.path().join("debian/control.in"),
                    Some(&td.path().join("debian"))
                )
            );
        }

        #[test]
        fn test_multiple_paragraphs() {
            let td = tempfile::tempdir().unwrap();
            std::fs::create_dir(td.path().join("debian")).unwrap();
            std::fs::write(
                td.path().join("debian/control.in"),
                r#"Source: blah
Build-Depends: debhelper, cdbs

Package: foo
"#,
            )
            .unwrap();
            assert_eq!(
                Some(super::TemplateType::Cdbs),
                super::guess_template_type(
                    &td.path().join("debian/control.in"),
                    Some(&td.path().join("debian"))
                )
            );
        }

        #[test]
        fn test_directory() {
            let td = tempfile::tempdir().unwrap();
            std::fs::create_dir(td.path().join("debian")).unwrap();
            std::fs::create_dir(td.path().join("debian/control.in")).unwrap();
            assert_eq!(
                Some(super::TemplateType::Directory),
                super::guess_template_type(
                    &td.path().join("debian/control.in"),
                    Some(&td.path().join("debian"))
                )
            );
        }

        #[test]
        fn test_debcargo() {
            let td = tempfile::tempdir().unwrap();
            std::fs::create_dir(td.path().join("debian")).unwrap();
            std::fs::write(
                td.path().join("debian/control.in"),
                r#"Source: blah
Build-Depends: bar
"#,
            )
            .unwrap();
            std::fs::write(
                td.path().join("debian/debcargo.toml"),
                r#"maintainer = Joe Example <joe@example.com>
"#,
            )
            .unwrap();
            assert_eq!(
                Some(super::TemplateType::Debcargo),
                super::guess_template_type(
                    &td.path().join("debian/control.in"),
                    Some(&td.path().join("debian"))
                )
            );
        }
    }

    #[test]
    fn test_postgresql() {
        let td = tempfile::tempdir().unwrap();
        std::fs::create_dir(td.path().join("debian")).unwrap();
        std::fs::write(
            td.path().join("debian/control.in"),
            r#"Source: blah
Build-Depends: bar, postgresql

Package: foo-PGVERSION
"#,
        )
        .unwrap();
        assert_eq!(
            Some(super::TemplateType::Postgresql),
            super::guess_template_type(
                &td.path().join("debian/control.in"),
                Some(&td.path().join("debian"))
            )
        );
    }

    #[test]
    fn test_apply_changes() {
        let mut deb822: deb822_lossless::Deb822 = r#"Source: blah
Build-Depends: debhelper (>= 6), foo

Package: bar
"#
        .parse()
        .unwrap();

        let mut changes = Deb822Changes(std::collections::HashMap::new());
        changes.0.insert(
            ("Source".to_string(), "blah".to_string()),
            vec![(
                "Build-Depends".to_string(),
                Some("debhelper (>= 6), foo".to_string()),
                Some("debhelper (>= 10), foo".to_string()),
            )],
        );

        super::apply_changes(&mut deb822, changes, None).unwrap();

        assert_eq!(
            deb822.to_string(),
            r#"Source: blah
Build-Depends: debhelper (>= 10), foo

Package: bar
"#
        );
    }

    #[test]
    fn test_apply_changes_new_paragraph() {
        let mut deb822: deb822_lossless::Deb822 = r#"Source: blah
Build-Depends: debhelper (>= 6), foo

Package: bar
"#
        .parse()
        .unwrap();

        let mut changes = Deb822Changes(std::collections::HashMap::new());
        changes.0.insert(
            ("Source".to_string(), "blah".to_string()),
            vec![(
                "Build-Depends".to_string(),
                Some("debhelper (>= 6), foo".to_string()),
                Some("debhelper (>= 10), foo".to_string()),
            )],
        );
        changes.0.insert(
            ("Package".to_string(), "blah2".to_string()),
            vec![
                ("Package".to_string(), None, Some("blah2".to_string())),
                (
                    "Description".to_string(),
                    None,
                    Some("Some package".to_string()),
                ),
            ],
        );

        super::apply_changes(&mut deb822, changes, None).unwrap();

        assert_eq!(
            deb822.to_string(),
            r#"Source: blah
Build-Depends: debhelper (>= 10), foo

Package: bar

Package: blah2
Description: Some package
"#
        );
    }

    #[test]
    fn test_apply_changes_conflict() {
        let mut deb822: deb822_lossless::Deb822 = r#"Source: blah
Build-Depends: debhelper (>= 6), foo

Package: bar
"#
        .parse()
        .unwrap();

        let mut changes = Deb822Changes(std::collections::HashMap::new());
        changes.0.insert(
            ("Source".to_string(), "blah".to_string()),
            vec![(
                "Build-Depends".to_string(),
                Some("debhelper (>= 7), foo".to_string()),
                Some("debhelper (>= 10), foo".to_string()),
            )],
        );

        let result = super::apply_changes(&mut deb822, changes, None);
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert_eq!(
            err,
            ChangeConflict {
                para_key: ("Source".to_string(), "blah".to_string()),
                field: "Build-Depends".to_string(),
                actual_old_value: Some("debhelper (>= 7), foo".to_string()),
                template_old_value: Some("debhelper (>= 6), foo".to_string()),
                actual_new_value: Some("debhelper (>= 10), foo".to_string()),
            }
        );
    }

    #[test]
    fn test_apply_changes_resolve_conflict() {
        let mut deb822: deb822_lossless::Deb822 = r#"Source: blah
Build-Depends: debhelper (>= 6), foo

Package: bar
"#
        .parse()
        .unwrap();

        let mut changes = Deb822Changes(std::collections::HashMap::new());
        changes.0.insert(
            ("Source".to_string(), "blah".to_string()),
            vec![(
                "Build-Depends".to_string(),
                Some("debhelper (>= 7), foo".to_string()),
                Some("debhelper (>= 10), foo".to_string()),
            )],
        );

        let result = super::apply_changes(&mut deb822, changes, Some(|_, _, _, _, _| Ok(None)));
        assert!(result.is_ok());
        assert_eq!(
            deb822.to_string(),
            r#"Source: blah

Package: bar
"#
        );
    }

    mod control_editor {
        #[test]
        fn test_do_not_edit() {
            let td = tempfile::tempdir().unwrap();
            std::fs::create_dir(td.path().join("debian")).unwrap();
            std::fs::write(
                td.path().join("debian/control"),
                r#"# DO NOT EDIT
# This file was generated by blah

Source: blah
Testsuite: autopkgtest

"#,
            )
            .unwrap();
            let mut editor =
                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
            editor.source().unwrap().set_name("foo");
            let changes = editor.changes();
            assert_eq!(
                changes.normalized(),
                vec![
                    (
                        ("Source", "blah"),
                        vec![
                            ("Source", Some("blah"), None),
                            ("Testsuite", Some("autopkgtest"), None),
                        ]
                    ),
                    (
                        ("Source", "foo"),
                        vec![
                            ("Source", None, Some("foo")),
                            ("Testsuite", None, Some("autopkgtest"))
                        ]
                    )
                ]
            );
            assert!(matches!(
                editor.commit().unwrap_err(),
                super::EditorError::GeneratedFile(_, _)
            ));
        }

        #[test]
        fn test_add_binary() {
            let td = tempfile::tempdir().unwrap();
            std::fs::create_dir(td.path().join("debian")).unwrap();
            std::fs::write(
                td.path().join("debian/control"),
                r#"Source: blah
Testsuite: autopkgtest

Package: blah
Description: Some description
 And there are more lines
 And more lines
"#,
            )
            .unwrap();
            let mut editor =
                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
            let mut binary = editor.add_binary("foo");
            binary.set_description(Some("A new package foo"));
            let paths = editor.commit().unwrap();
            assert_eq!(paths.len(), 1);
        }

        #[test]
        fn test_list_binaries() {
            let td = tempfile::tempdir().unwrap();
            std::fs::create_dir(td.path().join("debian")).unwrap();
            std::fs::write(
                td.path().join("debian/control"),
                r#"Source: blah
Testsuite: autopkgtest

Package: blah
Description: Some description
 And there are more lines
 And more lines
"#,
            )
            .unwrap();
            let mut editor =
                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
            let binaries = editor.binaries().collect::<Vec<_>>();
            assert_eq!(binaries.len(), 1);
            assert_eq!(binaries[0].name().as_deref(), Some("blah"));
            assert_eq!(editor.commit().unwrap(), Vec::<&std::path::Path>::new());
        }

        #[test]
        fn test_no_source() {
            let td = tempfile::tempdir().unwrap();
            std::fs::create_dir(td.path().join("debian")).unwrap();
            std::fs::write(
                td.path().join("debian/control"),
                r#"Package: blah
Testsuite: autopkgtest

Package: bar
"#,
            )
            .unwrap();
            let editor =
                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
            assert!(editor.source().is_none());
        }

        #[test]
        fn test_create() {
            let td = tempfile::tempdir().unwrap();
            std::fs::create_dir(td.path().join("debian")).unwrap();
            let mut editor =
                super::TemplatedControlEditor::create(td.path().join("debian/control")).unwrap();
            editor.add_source("foo");
            assert_eq!(
                r#"Source: foo
"#,
                editor.as_deb822().to_string()
            );
        }

        #[test]
        fn test_do_not_edit_no_change() {
            let td = tempfile::tempdir().unwrap();
            std::fs::create_dir(td.path().join("debian")).unwrap();
            std::fs::write(
                td.path().join("debian/control"),
                r#"# DO NOT EDIT
# This file was generated by blah

Source: blah
Testsuite: autopkgtest

"#,
            )
            .unwrap();
            let mut editor =
                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
            assert_eq!(editor.commit().unwrap(), Vec::<&std::path::Path>::new());
        }

        #[test]
        fn test_unpreservable() {
            let td = tempfile::tempdir().unwrap();
            std::fs::create_dir(td.path().join("debian")).unwrap();
            std::fs::write(
                td.path().join("debian/control"),
                r#"Source: blah
# A comment
Testsuite: autopkgtest

"#,
            )
            .unwrap();
            let mut editor =
                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
            editor
                .source()
                .unwrap()
                .as_mut_deb822()
                .set("NewField", "New Field");

            editor.commit().unwrap();

            assert_eq!(
                r#"Source: blah
# A comment
Testsuite: autopkgtest
NewField: New Field

"#,
                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
            );
        }

        #[test]
        fn test_modify_source() {
            let td = tempfile::tempdir().unwrap();
            std::fs::create_dir(td.path().join("debian")).unwrap();
            std::fs::write(
                td.path().join("debian/control"),
                r#"Source: blah
Testsuite: autopkgtest
"#,
            )
            .unwrap();

            let mut editor =
                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
            editor
                .source()
                .unwrap()
                .as_mut_deb822()
                .set("XS-Vcs-Git", "git://github.com/example/example");

            editor.commit().unwrap();

            assert_eq!(
                r#"Source: blah
Testsuite: autopkgtest
XS-Vcs-Git: git://github.com/example/example
"#,
                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
            );
        }

        /*
                #[test]
                fn test_wrap_and_sort() {
                    let td = tempfile::tempdir().unwrap();
                    std::fs::create_dir(td.path().join("debian")).unwrap();

                    std::fs::write(
                        td.path().join("debian/control"),
                        r#"Source: blah
        Testsuite: autopkgtest
        Depends: package3, package2

        Package: libblah
        Section: extra
        "#,
                    )
                    .unwrap();

                    let mut editor =
                        super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
                    editor.wrap_and_sort(trailing_comma = true).unwrap();

                    editor.commit().unwrap();

                    assert_eq!(
                        r#"Source: blah
        Testsuite: autopkgtest
        Depends: package2, package3,

        Package: libblah
        Section: extra
        "#,
                        std::fs::read_to_string(td.path().join("debian/control")).unwrap()
                    );
                }

                #[test]
                fn test_sort_binaries() {
                    let td = tempfile::tempdir().unwrap();
                    std::fs::create_dir(td.path().join("debian")).unwrap();
                    std::fs::write(
                        td.path().join("debian/control"),
                        r#"Source: blah
        Source: blah
        Testsuite: autopkgtest
        Depends: package3, package2

        Package: libfoo
        Section: web

        Package: libblah
        Section: extra
        "#,
                    )
                    .unwrap();

                    let mut editor =
                        super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();

                    editor.sort_binary_packages().unwrap();

                    editor.commit().unwrap();

                    assert_eq!(
                        r#"Source: blah
        Testsuite: autopkgtest
        Depends: package3, package2

        Package: libblah
        Section: extra

        Package: libfoo
        Section: web
        "#,
                        std::fs::read_to_string(td.path().join("debian/control")).unwrap()
                    );
                }

            */

        #[test]
        fn test_modify_binary() {
            let td = tempfile::tempdir().unwrap();
            std::fs::create_dir(td.path().join("debian")).unwrap();
            std::fs::write(
                td.path().join("debian/control"),
                r#"Source: blah
Testsuite: autopkgtest

Package: libblah
Section: extra
"#,
            )
            .unwrap();

            let mut editor =
                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
            let mut binary = editor
                .binaries()
                .find(|b| b.name().as_deref() == Some("libblah"))
                .unwrap();
            binary.set_architecture(Some("all"));

            editor.commit().unwrap();

            assert_eq!(
                r#"Source: blah
Testsuite: autopkgtest

Package: libblah
Architecture: all
Section: extra
"#,
                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
            );
        }

        #[test]
        fn test_doesnt_strip_whitespace() {
            let td = tempfile::tempdir().unwrap();
            std::fs::create_dir(td.path().join("debian")).unwrap();
            std::fs::write(
                td.path().join("debian/control"),
                r#"Source: blah
Testsuite: autopkgtest

"#,
            )
            .unwrap();
            let mut editor =
                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
            editor.commit().unwrap();

            assert_eq!(
                r#"Source: blah
Testsuite: autopkgtest

"#,
                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
            );
        }

        #[cfg(unix)]
        #[test]
        fn test_update_template() {
            use std::os::unix::fs::PermissionsExt;
            let td = tempfile::tempdir().unwrap();
            std::fs::create_dir(td.path().join("debian")).unwrap();
            std::fs::write(
                td.path().join("debian/control"),
                r#"# DO NOT EDIT
# This file was generated by blah

Source: blah
Testsuite: autopkgtest
Uploaders: Jelmer Vernooij <jelmer@jelmer.uk>

"#,
            )
            .unwrap();
            std::fs::write(
                td.path().join("debian/control.in"),
                r#"Source: blah
Testsuite: autopkgtest
Uploaders: @lintian-brush-test@

"#,
            )
            .unwrap();
            std::fs::write(
                td.path().join("debian/rules"),
                r#"#!/usr/bin/make -f

debian/control: debian/control.in
	sed -e 's/@lintian-brush-test@/testvalue/' < $< > $@
"#,
            )
            .unwrap();
            // Make debian/rules executable
            std::fs::set_permissions(
                td.path().join("debian/rules"),
                std::fs::Permissions::from_mode(0o755),
            )
            .unwrap();

            let mut editor =
                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
            editor
                .source()
                .unwrap()
                .as_mut_deb822()
                .set("Testsuite", "autopkgtest8");

            assert_eq!(
                editor.commit().unwrap(),
                vec![
                    td.path().join("debian/control.in"),
                    td.path().join("debian/control")
                ]
            );

            assert_eq!(
                r#"Source: blah
Testsuite: autopkgtest8
Uploaders: @lintian-brush-test@

"#,
                std::fs::read_to_string(td.path().join("debian/control.in")).unwrap()
            );

            assert_eq!(
                r#"Source: blah
Testsuite: autopkgtest8
Uploaders: testvalue

"#,
                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
            );
        }

        #[cfg(unix)]
        #[test]
        fn test_update_template_only() {
            use std::os::unix::fs::PermissionsExt;
            let td = tempfile::tempdir().unwrap();
            std::fs::create_dir(td.path().join("debian")).unwrap();
            std::fs::write(
                td.path().join("debian/control.in"),
                r#"Source: blah
Testsuite: autopkgtest
Uploaders: @lintian-brush-test@

"#,
            )
            .unwrap();
            std::fs::write(
                td.path().join("debian/rules"),
                r#"#!/usr/bin/make -f

debian/control: debian/control.in
	sed -e 's/@lintian-brush-test@/testvalue/' < $< > $@
"#,
            )
            .unwrap();

            std::fs::set_permissions(
                td.path().join("debian/rules"),
                std::fs::Permissions::from_mode(0o755),
            )
            .unwrap();

            let mut editor =
                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
            editor
                .source()
                .unwrap()
                .as_mut_deb822()
                .set("Testsuite", "autopkgtest8");

            editor.commit().unwrap();

            assert_eq!(
                r#"Source: blah
Testsuite: autopkgtest8
Uploaders: @lintian-brush-test@

"#,
                std::fs::read_to_string(td.path().join("debian/control.in")).unwrap()
            );

            assert!(!td.path().join("debian/control").exists());
        }

        #[cfg(unix)]
        #[test]
        fn test_update_template_invalid_tokens() {
            use std::os::unix::fs::PermissionsExt;
            let td = tempfile::tempdir().unwrap();
            std::fs::create_dir(td.path().join("debian")).unwrap();
            std::fs::write(
                td.path().join("debian/control"),
                r#"# DO NOT EDIT
# This file was generated by blah

Source: blah
Testsuite: autopkgtest
Uploaders: Jelmer Vernooij <jelmer@jelmer.uk>
"#,
            )
            .unwrap();
            std::fs::write(
                td.path().join("debian/control.in"),
                r#"Source: blah
Testsuite: autopkgtest
@OTHERSTUFF@
"#,
            )
            .unwrap();

            std::fs::write(
                td.path().join("debian/rules"),
                r#"#!/usr/bin/make -f

debian/control: debian/control.in
	sed -e 's/@OTHERSTUFF@/Vcs-Git: example.com/' < $< > $@
"#,
            )
            .unwrap();

            std::fs::set_permissions(
                td.path().join("debian/rules"),
                std::fs::Permissions::from_mode(0o755),
            )
            .unwrap();

            let mut editor =
                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
            editor
                .source()
                .unwrap()
                .as_mut_deb822()
                .set("Testsuite", "autopkgtest8");
            editor.commit().unwrap();

            assert_eq!(
                r#"Source: blah
Testsuite: autopkgtest8
@OTHERSTUFF@
"#,
                std::fs::read_to_string(td.path().join("debian/control.in")).unwrap()
            );

            assert_eq!(
                r#"Source: blah
Testsuite: autopkgtest8
Vcs-Git: example.com
"#,
                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
            );
        }

        #[test]
        fn test_update_cdbs_template() {
            let td = tempfile::tempdir().unwrap();
            std::fs::create_dir(td.path().join("debian")).unwrap();

            std::fs::write(
                td.path().join("debian/control"),
                r#"Source: blah
Testsuite: autopkgtest
Build-Depends: some-foo, libc6

"#,
            )
            .unwrap();

            std::fs::write(
                td.path().join("debian/control.in"),
                r#"Source: blah
Testsuite: autopkgtest
Build-Depends: @cdbs@, libc6

"#,
            )
            .unwrap();

            let mut editor =
                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();

            editor
                .source()
                .unwrap()
                .as_mut_deb822()
                .set("Build-Depends", "some-foo, libc6, some-bar");

            assert_eq!(
                editor
                    .source()
                    .unwrap()
                    .build_depends()
                    .unwrap()
                    .to_string(),
                "some-foo, libc6, some-bar".to_string()
            );

            assert_eq!(Some(super::TemplateType::Cdbs), editor.template_type());

            assert_eq!(
                editor.commit().unwrap(),
                vec![
                    td.path().join("debian/control.in"),
                    td.path().join("debian/control")
                ]
            );

            assert_eq!(
                r#"Source: blah
Testsuite: autopkgtest
Build-Depends: @cdbs@, libc6, some-bar

"#,
                std::fs::read_to_string(td.path().join("debian/control.in")).unwrap()
            );

            assert_eq!(
                r#"Source: blah
Testsuite: autopkgtest
Build-Depends: some-foo, libc6, some-bar

"#,
                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
            );
        }

        #[test]
        #[ignore = "Not implemented yet"]
        fn test_description_stays_last() {
            let td = tempfile::tempdir().unwrap();
            std::fs::create_dir(td.path().join("debian")).unwrap();
            std::fs::write(
                td.path().join("debian/control"),
                r#"Source: blah
Testsuite: autopkgtest

Package: libblah
Section: extra
Description: foo
 bar

"#,
            )
            .unwrap();

            let mut editor =
                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
            editor
                .binaries()
                .find(|b| b.name().as_deref() == Some("libblah"))
                .unwrap()
                .set_architecture(Some("all"));

            editor.commit().unwrap();

            assert_eq!(
                r#"Source: blah
Testsuite: autopkgtest

Package: libblah
Section: extra
Architecture: all
Description: foo
 bar
"#,
                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
            );
        }

        #[test]
        fn test_no_new_heading_whitespace() {
            let td = tempfile::tempdir().unwrap();
            std::fs::create_dir(td.path().join("debian")).unwrap();
            std::fs::write(
                td.path().join("debian/control"),
                r#"Source: blah
Build-Depends:
 debhelper-compat (= 11),
 uuid-dev

"#,
            )
            .unwrap();

            let mut editor =
                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
            editor
                .source()
                .unwrap()
                .as_mut_deb822()
                .set("Build-Depends", "debhelper-compat (= 12),\nuuid-dev");

            editor.commit().unwrap();

            assert_eq!(
                r#"Source: blah
Build-Depends:
 debhelper-compat (= 12),
 uuid-dev

"#,
                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
            );
        }

        #[test]
        fn test_control_clone_behavior() {
            use std::io::Cursor;

            let content = b"Source: test\nMaintainer: Joe <joe@example.com>\n";
            let (control1, _) =
                debian_control::Control::read_relaxed(Cursor::new(content)).unwrap();
            let control2 = control1.clone();

            println!(
                "control1 name before: {:?}",
                control1.source().unwrap().name()
            );
            println!(
                "control2 name before: {:?}",
                control2.source().unwrap().name()
            );

            // Modify control2
            control2.source().unwrap().set_name("changed");

            println!(
                "control1 name after: {:?}",
                control1.source().unwrap().name()
            );
            println!(
                "control2 name after: {:?}",
                control2.source().unwrap().name()
            );

            // If clone is deep, control1 should still be "test"
            // If clone is shallow, both will be "changed"
        }

        #[test]
        fn test_revert_before_commit() {
            use crate::editor::Editor;

            let td = tempfile::tempdir().unwrap();
            std::fs::create_dir(td.path().join("debian")).unwrap();
            std::fs::write(
                td.path().join("debian/control"),
                r#"Source: blah
Maintainer: Joe Developer <joe@example.com>

Package: blah
Architecture: any
Description: Some package
"#,
            )
            .unwrap();

            let mut editor =
                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();

            // Verify original state
            assert_eq!(editor.source().unwrap().name(), Some("blah".to_string()));
            assert!(!editor.has_changed());

            // Make a change
            editor.source().unwrap().set_name("newname");
            assert_eq!(editor.source().unwrap().name(), Some("newname".to_string()));
            assert!(editor.has_changed());

            // Revert the change
            editor.revert().unwrap();
            assert_eq!(editor.source().unwrap().name(), Some("blah".to_string()));
            assert!(!editor.has_changed());

            // File should not have changed
            assert_eq!(
                r#"Source: blah
Maintainer: Joe Developer <joe@example.com>

Package: blah
Architecture: any
Description: Some package
"#,
                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
            );
        }

        #[test]
        fn test_revert_after_commit() {
            use crate::editor::Editor;

            let td = tempfile::tempdir().unwrap();
            std::fs::create_dir(td.path().join("debian")).unwrap();
            std::fs::write(
                td.path().join("debian/control"),
                r#"Source: blah
Maintainer: Joe Developer <joe@example.com>

Package: blah
Architecture: any
Description: Some package
"#,
            )
            .unwrap();

            let mut editor =
                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();

            // Make a change and commit
            editor.source().unwrap().set_name("newname");
            assert!(editor.has_changed());
            editor.commit().unwrap();
            assert!(!editor.has_changed());
            assert_eq!(editor.source().unwrap().name(), Some("newname".to_string()));

            // Verify the file was updated
            let content = std::fs::read_to_string(td.path().join("debian/control")).unwrap();
            assert_eq!(
                r#"Source: newname
Maintainer: Joe Developer <joe@example.com>

Package: blah
Architecture: any
Description: Some package
"#,
                content
            );

            // Make another change
            editor.source().unwrap().set_name("thirdname");
            assert_eq!(
                editor.source().unwrap().name(),
                Some("thirdname".to_string())
            );
            assert!(editor.has_changed());

            // Revert should go back to committed state (newname, not original blah)
            editor.revert().unwrap();
            assert_eq!(editor.source().unwrap().name(), Some("newname".to_string()));
            assert!(!editor.has_changed());

            // File should still have committed content
            assert_eq!(
                r#"Source: newname
Maintainer: Joe Developer <joe@example.com>

Package: blah
Architecture: any
Description: Some package
"#,
                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
            );
        }
    }
}
