//! Tp-Note's low level API, creating a memory representation of a
//! note file by inserting Tp-Note's
//! environment data in some templates. If the note exists on disk already,
//! the memory representation is established be reading the note file and
//! parsing its front matter.
//! NB: The high level API is in the module `tpnote_lib::workflow`.

use crate::config::TMPL_VAR_DOC;
use crate::content::Content;
use crate::context::Context;
use crate::context::HasSettings;
use crate::context::ReadyForContentTemplate;
use crate::context::ReadyForFilenameTemplate;
use crate::error::NoteError;
use crate::filename::NotePath;
use crate::filename::NotePathBuf;
use crate::filter::TERA;
use crate::front_matter::FrontMatter;
use crate::note_error_tera_template;
use crate::template::TemplateKind;
use std::default::Default;
use std::fs;
use std::path::{Path, PathBuf};
use std::str;
use tera::Tera;

//#[derive(Debug, PartialEq)]
/// Represents a note.
/// 1. The `ContentString`'s header is deserialized into `FrontMatter`.
/// 2. `FrontMatter` is stored in `Context` with some environment data.
/// 3. `Context` data is filled in some filename template.
/// 4. The result is stored in `rendered_filename`.
pub struct Note<T: Content> {
    /// Captured environment of _Tp-Note_ that
    /// is used to fill in templates.
    pub context: Context<ReadyForFilenameTemplate>,
    /// The full text content of the note, including
    /// its front matter.
    pub content: T,
    /// This field equals to `PathBuf::new()` until `self.render_filename()`
    /// is called.
    pub rendered_filename: PathBuf,
}

impl<T: Content> Note<T> {
    /// Constructor creating a `Note` memory representation of some content
    /// that exists outside of Tp-Note provided by the `content` object. No file
    /// content is read from disk.
    ///
    /// In case of `TemplateKind::SyncFilename` the `content` is required to
    /// have a header with front matter.
    ///
    /// Contract: `template_kind` must be one of:
    /// * `TemplateKind::SyncFilename`,
    /// * `TemplateKind::None` or
    ///
    /// Panics otherwise. Use `Note::from_content_template()` in those cases.
    ///
    pub fn from_existing_content(
        context: Context<HasSettings>,
        content: T,
        template_kind: TemplateKind,
    ) -> Result<Note<T>, NoteError> {
        // Check contract.
        debug_assert!(match template_kind {
            TemplateKind::SyncFilename => true,

            TemplateKind::None => true,
            _ => panic!(
                "Contract violation: `template_kind=={:?}` is not acceptable here.",
                template_kind
            ),
        });

        // Deserialize the note's header read from disk.
        // Store the front matter in the context for later use in templates.
        let fm = FrontMatter::try_from(content.header())?;
        let context = context.insert_front_matter(&fm);

        // This data comes from outside. We need additional checks here.
        // For headerless files with `TemplateKind::None`, a missing required
        // field is not an error: the body is rendered as-is.
        match context.assert_precoditions() {
            Err(NoteError::FrontMatterFieldMissing { .. })
            | Err(NoteError::FrontMatterMissing { .. })
                if template_kind == TemplateKind::None && content.header().is_empty() => {}
            other => other?,
        }

        Ok(Note {
            context,
            content,
            rendered_filename: PathBuf::new(),
        })
    }

    /// Constructor that creates a new note by filling in the content
    /// template `template` with the data read from `context`.
    /// The result is an initialized `self.content`.
    ///
    /// Contract: `template_kind` should be NOT one of:
    /// * `TemplateKind::SyncFilename`,
    /// * `TemplateKind::None`
    ///
    /// Panics if this is the case.
    ///
    pub fn from_content_template(
        context: Context<ReadyForContentTemplate>,
        template_kind: TemplateKind,
    ) -> Result<Note<T>, NoteError> {
        // Add content to context.
        log::trace!(
            "Available substitution variables for the content template:\n{:#?}",
            *context
        );

        debug_assert!(match template_kind {
            TemplateKind::SyncFilename => panic!("`TemplateKind::SyncFilename` not allowed here"),

            TemplateKind::None => panic!("`TemplateKind::None` not allowed here"),
            _ => true,
        });

        // Render template
        let new_content: T = T::from_string(
            {
                let mut tera = Tera::default();
                tera.register_from(&TERA);

                // Panics, if the content template does not exist (see contract).
                // Returns an error, when the rendition goes wrong.
                tera.render_str(&template_kind.get_content_template(), &context, false)
                    .map_err(|e| {
                        note_error_tera_template!(
                            e,
                            template_kind.get_content_template_name().to_string()
                        )
                    })?
            },
            TMPL_VAR_DOC.to_string(),
        );

        log::debug!(
            "Rendered content template:\n---\n{}\n---\n\n{}",
            new_content.header(),
            new_content.body()
        );

        // Deserialize the rendered template
        let fm = FrontMatter::try_from(new_content.header())?;

        let new_context = Context::from_context_path(&context).insert_front_matter(&fm);

        // Return new note.
        Ok(Note {
            context: new_context,
            content: new_content,
            rendered_filename: PathBuf::new(),
        })
    }

    /// Applies a Tera template to the notes context in order to generate a
    /// sanitized filename that is in sync with the note's meta data stored in
    /// its front matter.
    pub fn render_filename(&mut self, template_kind: TemplateKind) -> Result<(), NoteError> {
        log::trace!(
            "Available substitution variables for the filename template:\n{:#?}",
            *self.context
        );
        // Render template
        let mut file_path = self.context.get_dir_path().to_owned();
        let mut tera = Tera::default();
        tera.register_from(&TERA);

        match tera.render_str(&template_kind.get_filename_template(), &self.context, false) {
            Ok(filename) => {
                file_path.push(filename.trim());
            }
            Err(e) => {
                return Err(note_error_tera_template!(
                    e,
                    template_kind.get_filename_template_name().to_string()
                ));
            }
        }

        file_path.shorten_filename();
        self.rendered_filename = file_path;
        Ok(())
    }

    /// Checks if `self.rendered_filename` is taken already.
    /// If yes, some copy counter is appended/incremented.
    /// Contract: `render_filename` must have been executed before.
    pub fn set_next_unused_rendered_filename(&mut self) -> Result<(), NoteError> {
        debug_assert_ne!(self.rendered_filename, PathBuf::new());

        self.rendered_filename.set_next_unused()?;
        Ok(())
    }

    /// Checks if `alt_path` is equal to `self.rendered_filename`
    /// without considering their copy counter.
    /// If they are similar, `self.rendered_filename` becomes `alt_path`.
    /// If they are different, then we continue incrementing the copy
    /// counter in `self.rendered_filename` until we find a free spot.
    /// (Same as in `set_next_unused_rendered_filename()`).
    /// Contract: `render_filename` must have been executed before.
    pub fn set_next_unused_rendered_filename_or(
        &mut self,
        alt_path: &Path,
    ) -> Result<(), NoteError> {
        debug_assert_ne!(self.rendered_filename, PathBuf::new());

        if self.rendered_filename.exclude_copy_counter_eq(alt_path) {
            self.rendered_filename = alt_path.to_path_buf();
        } else {
            self.rendered_filename.set_next_unused()?;
        }
        Ok(())
    }

    /// Writes the note to disk using the note's `content` and the note's
    /// `rendered_filename`.
    pub fn save(&self) -> Result<(), NoteError> {
        debug_assert_ne!(self.rendered_filename, PathBuf::new());

        log::trace!(
            "Writing the note's content to file: {:?}",
            self.rendered_filename
        );
        self.content.save_as(&self.rendered_filename)?;
        Ok(())
    }

    /// Rename the file `from_path` to `self.rendered_filename`.
    /// Silently fails is source and target are identical.
    /// Contract: `render_filename` must have been executed before.
    pub fn rename_file_from(&self, from_path: &Path) -> Result<(), NoteError> {
        debug_assert_ne!(self.rendered_filename, PathBuf::new());

        if !from_path.exclude_copy_counter_eq(&self.rendered_filename) {
            // Rename file
            fs::rename(from_path, &self.rendered_filename)?;
            log::trace!(
                "File renamed to {}",
                self.rendered_filename.to_str().unwrap_or_default()
            );
        }
        Ok(())
    }

    /// Write the note to disk and remove the file at the previous location.
    /// Similar to `rename_from()`, but the target is replaced by `self.content`.
    /// Silently fails is source and target are identical.
    /// Contract: `render_filename` must have been executed before.
    pub fn save_and_delete_from(&mut self, from_path: &Path) -> Result<(), NoteError> {
        debug_assert_ne!(self.rendered_filename, PathBuf::new());

        self.save()?;
        if from_path != self.rendered_filename {
            log::trace!("Deleting file: {:?}", from_path);
            fs::remove_file(from_path)?;
        }
        Ok(())
    }

    #[inline]
    /// Renders `self.content` to HTML by calling the appropriate markup
    /// renderer. The `html_tmpl` injects JavaScript code with the
    /// key `TMPL_HTML_VAR_VIEWER_DOC_JS`. This code is provided with
    /// `viewer_doc_js`.
    ///
    /// Contract:
    /// * `self.context` is in a valid `ReadyForFilenameTemplate` state.
    /// * `self.content.body_name == TMPL_VAR_DOC`. The HTML template expects
    ///   this name.
    /// * The `html_tmpl` template expects `content` to have a header with:
    ///   a `title:` field.
    ///
    pub fn render_content_to_html(
        &self,
        // HTML template for this rendition.
        tmpl: &str,
        // JavaScript for live update code injection.
        viewer_doc_js: &str,
    ) -> Result<String, NoteError> {
        //
        let html_context = self.context.clone();

        let html_context = html_context.insert_raw_content_and_css(&self.content, viewer_doc_js);

        log::trace!(
            "Available substitution variables for the HTML template:\
            \n{:#?}",
            html_context
        );

        let mut tera = Tera::default();
        tera.register_from(&TERA);
        let html = tera.render_str(tmpl, &html_context, true).map_err(|e| {
            note_error_tera_template!(e, "[html_tmpl] viewer/exporter_tmpl ".to_string())
        })?;

        Ok(html)
    }
}

#[cfg(test)]
mod tests {
    use super::Context;
    use super::FrontMatter;
    use crate::config::TMPL_VAR_FM_ALL;
    use serde_json::json;
    use std::path::Path;
    use tera::Value;

    #[test]
    fn test_deserialize() {
        let input = "# document start
        title:     The book
        subtitle:  you always wanted
        author:    It's me
        date:      2020-04-21
        lang:      en
        revision:  '1.0'
        sort_tag:  20200420-21_22
        file_ext:  md
        height:    1.23
        count:     2
        neg:       -1
        flag:      true
        numbers:
          - 1
          - 3
          - 5
        ";

        let mut expected = serde_json::Map::new();
        expected.insert("title".to_string(), json!("The book"));
        expected.insert("subtitle".to_string(), json!("you always wanted"));
        expected.insert("author".to_string(), json!("It\'s me"));
        expected.insert("date".to_string(), json!("2020-04-21"));
        expected.insert("lang".to_string(), json!("en"));
        expected.insert("revision".to_string(), json!("1.0"));
        expected.insert("sort_tag".to_string(), json!("20200420-21_22"));
        expected.insert("file_ext".to_string(), json!("md"));
        expected.insert("height".to_string(), json!(1.23));
        expected.insert("count".to_string(), json!(2));
        expected.insert("neg".to_string(), json!(-1));
        expected.insert("flag".to_string(), json!(true));
        expected.insert("numbers".to_string(), json!([1, 3, 5]));

        let expected_front_matter = FrontMatter(expected);

        //panic!("{:?}", &expected_front_matter);
        assert_eq!(expected_front_matter, FrontMatter::try_from(input).unwrap());
    }

    #[test]
    fn test_register_front_matter() {
        let mut tmp = serde_json::Map::new();
        tmp.insert("file_ext".to_string(), json!("md"));
        tmp.insert("height".to_string(), json!(1.23));
        tmp.insert("count".to_string(), json!(2));
        tmp.insert("neg".to_string(), json!(-1));
        tmp.insert("flag".to_string(), json!(true));
        tmp.insert("numbers".to_string(), json!([1, 3, 5]));

        let input1 = Context::from(Path::new("a/b/test.md")).unwrap();
        let input2 = FrontMatter(tmp);

        let mut expected = Context::from(Path::new("a/b/test.md")).unwrap();
        let tmp2 = Value::from_serializable(&serde_json::json!({
            "fm_file_ext": "md",
            "fm_height": 1.23,
            "fm_count": 2,
            "fm_neg": -1,
            "fm_flag": true,
            "fm_numbers": [1, 3, 5]
        }));
        expected.insert(TMPL_VAR_FM_ALL, &tmp2);
        let expected = expected.insert_front_matter(&FrontMatter::try_from("").unwrap());

        let result = input1.insert_front_matter(&input2);

        assert_eq!(result, expected);
    }

    #[test]
    fn test_from_existing_content1() {
        //
        // Example with `TemplateKind::SyncFilename`
        //
        use crate::content::Content;
        use crate::content::ContentString;
        use crate::context::Context;
        use crate::note::Note;
        use crate::template::TemplateKind;
        use std::env::temp_dir;
        use std::fs;
        // Prepare test: create existing note.
        let raw = r#"---
title: "My day"
subtitle: "Note"
---
Body text
"#;
        let notefile = temp_dir().join("20221031-hello.md");
        fs::write(&notefile, raw.as_bytes()).unwrap();
        let expected = temp_dir().join("20221031-My day--Note.md");
        let _ = fs::remove_file(&expected);
        // Start test.
        let context = Context::from(&notefile).unwrap();
        // Create note object.
        let content = <ContentString as Content>::open(&notefile).unwrap();
        // You can plug in your own type (must impl. `Content`).
        let mut n = Note::<ContentString>::from_existing_content(
            context,
            content,
            TemplateKind::SyncFilename,
        )
        .unwrap();

        let path = n.context.get_path().to_owned();
        n.render_filename(TemplateKind::SyncFilename).unwrap();
        n.set_next_unused_rendered_filename_or(&path).unwrap();
        assert_eq!(n.rendered_filename, expected);
        // Rename file on the disk.
        n.rename_file_from(&path).unwrap();
        assert!(n.rendered_filename.is_file());
    }

    #[test]
    fn test_from_existing_content2() {
        // Example with `TemplateKind::None`
        //
        //    This constructor is called, when `Note` is solely created for
        // HTML rendering and no templates will be applied.
        //
        use crate::config::LIB_CFG;
        use crate::config::TMPL_HTML_VAR_VIEWER_DOC_JS;
        use crate::content::Content;
        use crate::content::ContentString;
        use crate::context::Context;
        use crate::note::Note;
        use crate::template::TemplateKind;
        use std::env::temp_dir;
        use std::fs;
        // Prepare test: create existing note file.
        let raw = r#"---
title: "My day"
subtitle: "Note"
---
Body text
"#;
        let notefile = temp_dir().join("20221030-My day--Note.md");
        fs::write(&notefile, raw.as_bytes()).unwrap();
        // Start test
        // Only minimal context is needed, because no templates are applied later.
        let mut context = Context::from(&notefile).unwrap();
        // We do not inject any JavaScript.
        context.insert(TMPL_HTML_VAR_VIEWER_DOC_JS, &"");
        // Create note object.
        let content = <ContentString as Content>::open(&notefile).unwrap();
        // You can plug in your own type (must impl. `Content`).
        let n: Note<ContentString> =
            Note::<ContentString>::from_existing_content(context, content, TemplateKind::None)
                .unwrap();
        // Check the HTML rendition.
        let html = n
            .render_content_to_html(&LIB_CFG.read_recursive().tmpl_html.viewer, "")
            .unwrap();
        assert!(html.starts_with("<!DOCTYPE html>\n<html"))
    }

    #[test]
    fn test_from_content_template1() {
        // Example with `TemplateKind::New`
        //
        use crate::content::Content;
        use crate::content::ContentString;
        use crate::context::Context;
        use crate::note::Note;
        use crate::settings::SETTINGS;
        use crate::settings::Settings;
        use crate::template::TemplateKind;
        use parking_lot::RwLockWriteGuard;
        use std::env::temp_dir;
        use std::fs;

        // Prepare test.
        let mut settings = SETTINGS.write();
        *settings = Settings::default();
        // This locks `SETTINGS` for further write access in this scope.
        let _settings = RwLockWriteGuard::<'_, _>::downgrade(settings);
        // Create a directory for the new note.
        let notedir = temp_dir().join("123-my dir/");
        fs::create_dir_all(&notedir).unwrap();

        // Store the path in `context`.
        let context = Context::from(&notedir).unwrap();
        let html_clipboard =
            ContentString::from_string("".to_string(), "html_clipboard".to_string());
        let txt_clipboard = ContentString::from_string("".to_string(), "txt_clipboard".to_string());
        let stdin = ContentString::from_string("".to_string(), "stdin".to_string());
        let v = vec![&html_clipboard, &txt_clipboard, &stdin];
        let context = context
            .insert_front_matter_and_raw_text_from_existing_content(&v)
            .unwrap()
            .set_state_ready_for_content_template();

        // Create the `Note` object.
        // You can plug in your own type (must impl. `Content`).
        let mut n =
            Note::<ContentString>::from_content_template(context, TemplateKind::FromDir).unwrap();
        assert!(n.content.header().starts_with("title:        my dir"));
        assert_eq!(n.content.borrow_dependent().body, "\n\n");

        // Check the title and subtitle in the note's header.
        assert_eq!(
            n.context
                .get(TMPL_VAR_FM_ALL)
                .unwrap()
                .get_from_path("fm_title")
                .unwrap()
                .as_str(),
            Some("my dir")
        );
        assert_eq!(
            n.context
                .get(TMPL_VAR_FM_ALL)
                .unwrap()
                .get_from_path("fm_subtitle")
                .unwrap()
                .as_str(),
            Some("Note")
        );
        n.render_filename(TemplateKind::FromDir).unwrap();
        n.set_next_unused_rendered_filename().unwrap();
        n.save().unwrap();

        // Check the created new note file.
        assert!(n.rendered_filename.is_file());
        let raw_note = fs::read_to_string(n.rendered_filename).unwrap();
        #[cfg(not(target_family = "windows"))]
        assert!(raw_note.starts_with("\u{feff}---\ntitle:        my dir"));
        #[cfg(target_family = "windows")]
        assert!(raw_note.starts_with("\u{feff}---\r\ntitle:        my dir"));
    }

    #[test]
    fn test_from_content_template2() {
        // Example with `TemplateKind::FromClipboard`

        use crate::content::Content;
        use crate::content::ContentString;
        use crate::context::Context;
        use crate::note::Note;
        use crate::settings::SETTINGS;
        use crate::settings::Settings;
        use crate::template::TemplateKind;
        use parking_lot::RwLockWriteGuard;
        use std::env::temp_dir;
        use std::fs;

        // Prepare test.
        let mut settings = SETTINGS.write();
        *settings = Settings::default();
        // This locks `SETTINGS` for further write access in this scope.
        let _settings = RwLockWriteGuard::<'_, _>::downgrade(settings);
        // Directory for the new note.
        let notedir = temp_dir();

        // Store the path in `context`.
        let context = Context::from(&notedir).unwrap();
        let html_clipboard =
            ContentString::from_string("html_clp\n".to_string(), "html_clipboard".to_string());
        let txt_clipboard =
            ContentString::from_string("txt_clp\n".to_string(), "txt_clipboard".to_string());
        let stdin = ContentString::from_string("std\n".to_string(), "stdin".to_string());
        let v = vec![&html_clipboard, &txt_clipboard, &stdin];
        let context = context
            .insert_front_matter_and_raw_text_from_existing_content(&v)
            .unwrap();

        // This is the condition to choose: `TemplateKind::FromClipboard`:
        assert!(
            html_clipboard.header().is_empty()
                && txt_clipboard.header().is_empty()
                && stdin.header().is_empty()
        );
        assert!(
            !html_clipboard.body().is_empty()
                && !txt_clipboard.body().is_empty()
                && !stdin.body().is_empty()
        );

        let context = context.set_state_ready_for_content_template();

        // Create the `Note` object.
        // You can plug in your own type (must impl. `Content`).
        let mut n =
            Note::<ContentString>::from_content_template(context, TemplateKind::FromDir).unwrap();
        let expected_body = "\nstd\ntxt_clp\n\n";
        assert_eq!(n.content.body(), expected_body);
        // Check the title and subtitle in the note's header.
        assert_eq!(
            n.context
                .get(TMPL_VAR_FM_ALL)
                .unwrap()
                .get_from_path("fm_title")
                .unwrap()
                .as_str(),
            Some("std")
        );

        assert_eq!(
            n.context
                .get(TMPL_VAR_FM_ALL)
                .unwrap()
                .get_from_path("fm_subtitle")
                .unwrap()
                .as_str(),
            Some("Note")
        );
        n.render_filename(TemplateKind::FromDir).unwrap();
        n.set_next_unused_rendered_filename().unwrap();
        n.save().unwrap();

        // Check the new note file.
        // println!("{:?}", n.rendered_filename);
        assert!(
            n.rendered_filename
                .as_os_str()
                .to_str()
                .unwrap()
                .contains("std--Note")
        );
        assert!(n.rendered_filename.is_file());
        let raw_note = fs::read_to_string(&n.rendered_filename).unwrap();
        println!("{}", raw_note);
        #[cfg(not(target_family = "windows"))]
        assert!(raw_note.starts_with("\u{feff}---\ntitle:        std"));
        #[cfg(target_family = "windows")]
        assert!(raw_note.starts_with("\u{feff}---\r\ntitle:"));
    }

    #[test]
    fn test_from_content_template3() {
        // Example with `TemplateKind::FromClipboardYaml`

        use crate::content::Content;
        use crate::content::ContentString;
        use crate::context::Context;
        use crate::note::Note;
        use crate::settings::SETTINGS;
        use crate::settings::Settings;
        use crate::template::TemplateKind;
        use parking_lot::RwLockWriteGuard;
        use std::env::temp_dir;
        use std::fs;

        // Prepare test.
        let mut settings = SETTINGS.write();
        *settings = Settings::default();
        // This locks `SETTINGS` for further write access in this scope.
        let _settings = RwLockWriteGuard::<'_, _>::downgrade(settings);
        // Directory for the new note.
        let notedir = temp_dir().join("123-my dir/");

        // Run test.
        // Store the path in `context`.
        let context = Context::from(&notedir).unwrap();
        let html_clipboard = ContentString::from_string(
            "my HTML clipboard\n".to_string(),
            "html_clipboard".to_string(),
        );
        let txt_clipboard = ContentString::from_string(
            "my TXT clipboard\n".to_string(),
            "txt_clipboard".to_string(),
        );
        let stdin = ContentString::from_string(
            "---\nsubtitle: \"this overwrites\"\n---\nstdin body".to_string(),
            "stdin".to_string(),
        );
        let v = vec![&html_clipboard, &txt_clipboard, &stdin];

        let context = context
            .insert_front_matter_and_raw_text_from_existing_content(&v)
            .unwrap();
        // This is the condition to choose: `TemplateKind::FromClipboardYaml`:
        assert!(
            !html_clipboard.header().is_empty()
                || !txt_clipboard.header().is_empty()
                || !stdin.header().is_empty()
        );

        let context = context.set_state_ready_for_content_template();

        // Create the `Note` object.
        // You can plug in your own type (must impl. `Content`).
        let mut n =
            Note::<ContentString>::from_content_template(context, TemplateKind::FromDir).unwrap();
        let expected_body = "\nstdin body\nmy TXT clipboard\n\n";
        assert_eq!(n.content.body(), expected_body);
        // Check the title and subtitle in the note's header.
        assert_eq!(
            n.context
                .get(TMPL_VAR_FM_ALL)
                .unwrap()
                .get_from_path("fm_title")
                .unwrap()
                .as_str(),
            // Remember: in debug titles are very short. The code only works,
            // because the string is pure ASCII (not UTF-8).
            Some("stdin bod")
        );

        assert_eq!(
            n.context
                .get(TMPL_VAR_FM_ALL)
                .unwrap()
                .get_from_path("fm_subtitle")
                .unwrap()
                .as_str(),
            // Remember: in debug titles are very short. The code only works,
            // because the string is pure ASCII (not UTF-8).
            Some("this over")
        );
        n.render_filename(TemplateKind::FromDir).unwrap();
        n.set_next_unused_rendered_filename().unwrap();
        n.save().unwrap();

        // Check the new note file.
        //println!("rendered_filename == {:?}", n.rendered_filename);
        assert!(
            n.rendered_filename
                .as_os_str()
                .to_str()
                .unwrap()
                .contains("stdin bod--this over")
        );
        assert!(n.rendered_filename.is_file());
        let raw_note = fs::read_to_string(n.rendered_filename).unwrap();
        #[cfg(not(target_family = "windows"))]
        assert!(raw_note.starts_with("\u{feff}---\ntitle:        stdin bod"));
        #[cfg(target_family = "windows")]
        assert!(raw_note.starts_with("\u{feff}---\r\ntitle:        stdin bod"));
    }

    #[test]
    fn test_from_content_template4() {
        // Example with `TemplateKind::AnnotateFile`

        use crate::content::Content;
        use crate::content::ContentString;
        use crate::context::Context;
        use crate::note::Note;
        use crate::settings::SETTINGS;
        use crate::settings::Settings;
        use crate::template::TemplateKind;
        use parking_lot::RwLockWriteGuard;
        use std::env::temp_dir;
        use std::fs;

        // Prepare the test.
        let mut settings = SETTINGS.write();
        *settings = Settings::default();
        // This locks `SETTINGS` for further write access in this scope.
        let _settings = RwLockWriteGuard::<'_, _>::downgrade(settings);

        // Create some non-Tp-Note-file.
        let raw = "This simulates a non tp-note file";
        let non_notefile = temp_dir().join("20221030-some.pdf");
        fs::write(&non_notefile, raw.as_bytes()).unwrap();
        let expected = temp_dir().join("20221030-some.pdf--Note.md");
        let _ = fs::remove_file(&expected);

        // Run the test.
        // Store the path in `context`.
        let context = Context::from(&non_notefile).unwrap();
        let html_clipboard = ContentString::from_string(
            "my HTML clipboard\n".to_string(),
            "html_clipboard".to_string(),
        );
        let txt_clipboard = ContentString::from_string(
            "my TXT clipboard\n".to_string(),
            "txt_clipboard".to_string(),
        );
        let stdin = ContentString::from_string("my stdin\n".to_string(), "stdin".to_string());
        let v = vec![&html_clipboard, &txt_clipboard, &stdin];

        let context = context
            .insert_front_matter_and_raw_text_from_existing_content(&v)
            .unwrap()
            .set_state_ready_for_content_template();

        // Create the `Note` object.
        // You can plug in your own type (must impl. `Content`).
        let mut n =
            Note::<ContentString>::from_content_template(context, TemplateKind::AnnotateFile)
                .unwrap();
        let expected_body =
            "\n[20221030-some.pdf](<20221030-some.pdf>)\n____\n\nmy stdin\nmy TXT clipboard\n\n";
        assert_eq!(n.content.body(), expected_body);
        // Check the title and subtitle in the note's header.
        assert_eq!(
            n.context
                .get(TMPL_VAR_FM_ALL)
                .unwrap()
                .get_from_path("fm_title")
                .unwrap()
                .as_str(),
            Some("some.pdf")
        );
        assert_eq!(
            n.context
                .get(TMPL_VAR_FM_ALL)
                .unwrap()
                .get_from_path("fm_subtitle")
                .unwrap()
                .as_str(),
            Some("Note")
        );

        n.render_filename(TemplateKind::AnnotateFile).unwrap();
        n.set_next_unused_rendered_filename().unwrap();
        n.save().unwrap();

        // Check the new note file.
        assert_eq!(n.rendered_filename, expected);
        fs::remove_file(n.rendered_filename).unwrap();
    }

    #[test]
    fn test_from_existing_content5() {
        //
        // Example with `TemplateKind::FromTextFile`
        //
        use crate::content::Content;
        use crate::content::ContentString;
        use crate::context::Context;
        use crate::note::Note;
        use crate::template::TemplateKind;
        use std::env::temp_dir;
        use std::fs;

        // Prepare test: create existing note file without header.
        let raw = "Body text without header";
        let notefile = temp_dir().join("20221030-hello -- world.md");
        let _ = fs::write(&notefile, raw.as_bytes());
        let expected = temp_dir().join("20221030-hello--world.md");
        let _ = fs::remove_file(&expected);
        // Start test.
        let context = Context::from(&notefile).unwrap();
        // Create note object.
        let content = <ContentString as Content>::open(&notefile).unwrap();
        let context = context
            .insert_front_matter_and_raw_text_from_existing_content(&vec![&content])
            .unwrap()
            .set_state_ready_for_content_template();
        // You can plug in your own type (must impl. `Content`).
        let mut n = Note::<ContentString>::from_content_template(
            context.clone(),
            TemplateKind::FromTextFile,
        )
        .unwrap();
        assert!(!n.content.header().is_empty());
        assert_eq!(
            n.context
                .get(TMPL_VAR_FM_ALL)
                .unwrap()
                .get_from_path("fm_title")
                .unwrap()
                .as_str(),
            Some("hello ")
        );
        assert_eq!(
            n.context
                .get(TMPL_VAR_FM_ALL)
                .unwrap()
                .get_from_path("fm_subtitle")
                .unwrap()
                .as_str(),
            Some(" world")
        );
        assert_eq!(n.content.body().trim(), raw);
        n.render_filename(TemplateKind::FromTextFile).unwrap();
        n.set_next_unused_rendered_filename().unwrap();
        n.save_and_delete_from(context.get_path()).unwrap();

        // Check the new file with header
        assert_eq!(&n.rendered_filename, &expected);
        assert!(n.rendered_filename.is_file());
        let raw_note = fs::read_to_string(n.rendered_filename).unwrap();
        #[cfg(not(target_family = "windows"))]
        assert!(raw_note.starts_with("\u{feff}---\ntitle:        'hello '"));
        #[cfg(target_family = "windows")]
        assert!(raw_note.starts_with("\u{feff}---\r\ntitle:        'hello '"));
    }

    /// A file without YAML front matter rendered with `TemplateKind::None`
    /// must succeed: the missing-header errors are suppressed so the body
    /// is rendered as-is.
    #[test]
    fn test_from_existing_content_no_header() {
        use crate::content::Content;
        use crate::content::ContentString;
        use crate::context::Context;
        use crate::note::Note;
        use crate::template::TemplateKind;
        use std::env::temp_dir;
        use std::fs;

        let raw = "Body text without any YAML front matter.";
        let notefile = temp_dir().join("20221030-headerless-test.rst");
        fs::write(&notefile, raw.as_bytes()).unwrap();

        let context = Context::from(&notefile).unwrap();
        let content = <ContentString as Content>::open(&notefile).unwrap();
        assert!(content.header().is_empty());

        let result =
            Note::<ContentString>::from_existing_content(context, content, TemplateKind::None);
        assert!(
            result.is_ok(),
            "Expected Ok for headerless file with TemplateKind::None, got: {:?}",
            result.err()
        );
    }

    /// Valid Markdown in the note body renders successfully through a custom
    /// `markup_to_html` template call.
    #[test]
    #[cfg(feature = "renderer")]
    fn test_render_content_to_html_md_ok() {
        use crate::content::Content;
        use crate::content::ContentString;
        use crate::context::Context;
        use crate::note::Note;
        use crate::template::TemplateKind;
        use std::env::temp_dir;
        use std::fs;

        let raw = "---\ntitle: Markdown ok test\n---\n# Hello\nWorld";
        let notefile = temp_dir().join("20221030-md-ok-test.md");
        fs::write(&notefile, raw.as_bytes()).unwrap();

        let context = Context::from(&notefile).unwrap();
        let content = <ContentString as Content>::open(&notefile).unwrap();
        let n = Note::<ContentString>::from_existing_content(context, content, TemplateKind::None)
            .unwrap();

        let tmpl = "{{ doc.body | markup_to_html(extension='md') | safe }}";
        let html = n.render_content_to_html(tmpl, "").unwrap();
        assert!(
            html.contains("<h1>Hello</h1>"),
            "unexpected Markdown output: {html}"
        );
    }

    /// Valid RST in the note body renders successfully through a custom
    /// `markup_to_html` template call.
    #[test]
    #[cfg(feature = "renderer")]
    fn test_render_content_to_html_rst_ok() {
        use crate::content::Content;
        use crate::content::ContentString;
        use crate::context::Context;
        use crate::note::Note;
        use crate::template::TemplateKind;
        use std::env::temp_dir;
        use std::fs;

        let raw = "---\ntitle: RST ok test\n---\n`Link text <https://domain.invalid/>`_";
        let notefile = temp_dir().join("20221030-rst-ok-test.rst");
        fs::write(&notefile, raw.as_bytes()).unwrap();

        let context = Context::from(&notefile).unwrap();
        let content = <ContentString as Content>::open(&notefile).unwrap();
        let n = Note::<ContentString>::from_existing_content(context, content, TemplateKind::None)
            .unwrap();

        let tmpl = "{{ doc.body | markup_to_html(extension='rst') | safe }}";
        let html = n.render_content_to_html(tmpl, "").unwrap();
        assert!(
            html.contains("<a href=\"https://domain.invalid/\">Link text</a>"),
            "unexpected RST output: {html}"
        );
    }

    /// A broken markup_to_html call inside a viewer/exporter template must
    /// produce `NoteError::MarkupError`, not the generic `NoteError::TeraTemplate`.
    /// This exercises the `starts_with("Filter call 'markup_to_html'")` branch
    /// inside `note_error_tera_template!`.
    #[test]
    #[cfg(feature = "renderer")]
    fn test_render_content_to_html_markup_error() {
        use crate::content::Content;
        use crate::content::ContentString;
        use crate::context::Context;
        use crate::error::NoteError;
        use crate::note::Note;
        use crate::template::TemplateKind;
        use std::env::temp_dir;
        use std::fs;

        // Note with a well-formed header so assert_preconditions() passes,
        // and a body whose RST contains an unresolved substitution reference
        // that panics inside rst_renderer.
        let raw = "---\ntitle: Markup error test\n---\nThe |undefined| substitution reference.";
        let notefile = temp_dir().join("20221030-markup-error-test.rst");
        fs::write(&notefile, raw.as_bytes()).unwrap();

        let context = Context::from(&notefile).unwrap();
        let content = <ContentString as Content>::open(&notefile).unwrap();
        let n = Note::<ContentString>::from_existing_content(context, content, TemplateKind::None)
            .unwrap();

        // Minimal template that routes the body through the RST renderer.
        let tmpl = "{{ doc.body | markup_to_html(extension='rst') | safe }}";
        let result = n.render_content_to_html(tmpl, "");

        assert!(result.is_err());
        assert!(
            matches!(result.unwrap_err(), NoteError::MarkupError { .. }),
            "Expected NoteError::MarkupError from a failing markup_to_html filter"
        );
    }
}
