//! 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,
}

#[derive(Debug)]
enum TemplateExpansionError {
    Failed(String),
    ExpandCommandMissing(String),
    UnknownTemplating(PathBuf, Option<PathBuf>),
    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
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
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() {
        for item in paragraph.items().collect::<Vec<_>>() {
            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_analyzer::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> {
        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)
    }

    /// Open an existing control file.
    pub fn open<P: AsRef<Path>>(control_path: P) -> Result<Self, EditorError> {
        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> {
        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().clone();
                    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(&self) -> Result<Vec<PathBuf>, EditorError> {
        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(&self) -> Result<Vec<std::path::PathBuf>, EditorError> {
        TemplatedControlEditor::commit(self)
    }
}

#[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@, foo, debhelper (>= 10)".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()));
    }

    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 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 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 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 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 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 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
Section: extra
Architecture: all
"#,
                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 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 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 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 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 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 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 editor =
                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
            editor
                .source()
                .unwrap()
                .as_mut_deb822()
                .set("Build-Depends", "\ndebhelper-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()
            );
        }
    }
}
