use crate::{
    content::{self, json, yaml, Content},
    elog,
    utils::style,
};
use once_cell::sync::Lazy;
use std::env;
use std::error::Error;
use std::fmt;
use std::fs;
use std::io::{self, BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::time::{SystemTime, UNIX_EPOCH};
use std::{borrow::Cow, iter::once};

static RUN_ID: Lazy<String> = Lazy::new(|| {
    if let Ok(run_id) = env::var("NEXTEST_RUN_ID") {
        run_id
    } else {
        let d = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
        format!("{}-{}", d.as_secs(), d.subsec_nanos())
    }
});

/// Holds a pending inline snapshot loaded from a json file or read from an assert
/// macro (doesn't write to the rust file, which is done by `cargo-insta`)
#[derive(Debug)]
pub struct PendingInlineSnapshot {
    pub run_id: String,
    pub line: u32,
    pub new: Option<Snapshot>,
    pub old: Option<Snapshot>,
}

impl PendingInlineSnapshot {
    pub fn new(new: Option<Snapshot>, old: Option<Snapshot>, line: u32) -> PendingInlineSnapshot {
        PendingInlineSnapshot {
            new,
            old,
            line,
            run_id: RUN_ID.clone(),
        }
    }

    #[cfg(feature = "_cargo_insta_internal")]
    pub fn load_batch(p: &Path) -> Result<Vec<PendingInlineSnapshot>, Box<dyn Error>> {
        let contents =
            fs::read_to_string(p).map_err(|e| content::Error::FileIo(e, p.to_path_buf()))?;

        let mut rv: Vec<Self> = contents
            .lines()
            .map(|line| {
                let value = yaml::parse_str(line, p)?;
                Self::from_content(value)
            })
            .collect::<Result<_, Box<dyn Error>>>()?;

        // remove all but the last run
        if let Some(last_run_id) = rv.last().map(|x| x.run_id.clone()) {
            rv.retain(|x| x.run_id == last_run_id);
        }

        Ok(rv)
    }

    #[cfg(feature = "_cargo_insta_internal")]
    pub fn save_batch(p: &Path, batch: &[PendingInlineSnapshot]) -> Result<(), Box<dyn Error>> {
        fs::remove_file(p).ok();
        for snap in batch {
            snap.save(p)?;
        }
        Ok(())
    }

    pub fn save(&self, p: &Path) -> Result<(), Box<dyn Error>> {
        // Create parent directories if they don't exist (needed for INSTA_PENDING_DIR)
        if let Some(parent) = p.parent() {
            fs::create_dir_all(parent)?;
        }
        let mut f = fs::OpenOptions::new().create(true).append(true).open(p)?;
        let mut s = json::to_string(&self.as_content());
        s.push('\n');
        f.write_all(s.as_bytes())?;
        Ok(())
    }

    #[cfg(feature = "_cargo_insta_internal")]
    fn from_content(content: Content) -> Result<PendingInlineSnapshot, Box<dyn Error>> {
        if let Content::Map(map) = content {
            let mut run_id = None;
            let mut line = None;
            let mut old = None;
            let mut new = None;

            for (key, value) in map.into_iter() {
                match key.as_str() {
                    Some("run_id") => run_id = value.as_str().map(|x| x.to_string()),
                    Some("line") => line = value.as_u64().map(|x| x as u32),
                    Some("old") if !value.is_nil() => {
                        old = Some(Snapshot::from_content(value, TextSnapshotKind::Inline)?)
                    }
                    Some("new") if !value.is_nil() => {
                        new = Some(Snapshot::from_content(value, TextSnapshotKind::Inline)?)
                    }
                    _ => {}
                }
            }

            Ok(PendingInlineSnapshot {
                run_id: run_id.ok_or(content::Error::MissingField)?,
                line: line.ok_or(content::Error::MissingField)?,
                new,
                old,
            })
        } else {
            Err(content::Error::UnexpectedDataType.into())
        }
    }

    fn as_content(&self) -> Content {
        let fields = vec![
            ("run_id", Content::from(self.run_id.as_str())),
            ("line", Content::from(self.line)),
            (
                "new",
                match &self.new {
                    Some(snap) => snap.as_content(),
                    None => Content::None,
                },
            ),
            (
                "old",
                match &self.old {
                    Some(snap) => snap.as_content(),
                    None => Content::None,
                },
            ),
        ];

        Content::Struct("PendingInlineSnapshot", fields)
    }
}

#[derive(Debug, Clone, PartialEq, Default)]
pub enum SnapshotKind {
    #[default]
    Text,
    Binary {
        extension: String,
    },
}

/// Snapshot metadata information.
#[derive(Debug, Default, Clone, PartialEq)]
pub struct MetaData {
    /// The source file (relative to workspace root).
    pub(crate) source: Option<String>,
    /// The source line, if available. This is used by pending snapshots, but trimmed
    /// before writing to the final `.snap` files in [`MetaData::trim_for_persistence`].
    pub(crate) assertion_line: Option<u32>,
    /// Optional human readable (non formatted) snapshot description.
    pub(crate) description: Option<String>,
    /// Optionally the expression that created the snapshot.
    pub(crate) expression: Option<String>,
    /// An optional arbitrary structured info object.
    pub(crate) info: Option<Content>,
    /// Reference to the input file.
    pub(crate) input_file: Option<String>,
    /// The type of the snapshot (string or binary).
    pub(crate) snapshot_kind: SnapshotKind,
}

impl MetaData {
    /// Returns the absolute source path.
    pub fn source(&self) -> Option<&str> {
        self.source.as_deref()
    }

    /// Returns the assertion line.
    pub fn assertion_line(&self) -> Option<u32> {
        self.assertion_line
    }

    /// Returns the expression that created the snapshot.
    pub fn expression(&self) -> Option<&str> {
        self.expression.as_deref()
    }

    /// Returns the description that created the snapshot.
    pub fn description(&self) -> Option<&str> {
        self.description.as_deref().filter(|x| !x.is_empty())
    }

    /// Returns the embedded info.
    #[doc(hidden)]
    pub fn private_info(&self) -> Option<&Content> {
        self.info.as_ref()
    }

    /// Returns the relative source path.
    pub fn get_relative_source(&self, base: &Path) -> Option<PathBuf> {
        self.source.as_ref().map(|source| {
            base.join(source)
                .canonicalize()
                .ok()
                .and_then(|s| s.strip_prefix(base).ok().map(|x| x.to_path_buf()))
                .unwrap_or_else(|| base.to_path_buf())
        })
    }

    /// Returns the input file reference.
    pub fn input_file(&self) -> Option<&str> {
        self.input_file.as_deref()
    }

    fn from_content(content: Content) -> Result<MetaData, Box<dyn Error>> {
        if let Content::Map(map) = content {
            let mut source = None;
            let mut assertion_line = None;
            let mut description = None;
            let mut expression = None;
            let mut info = None;
            let mut input_file = None;
            let mut snapshot_type = TmpSnapshotKind::Text;
            let mut extension = None;

            enum TmpSnapshotKind {
                Text,
                Binary,
            }

            for (key, value) in map.into_iter() {
                match key.as_str() {
                    Some("source") => source = value.as_str().map(|x| x.to_string()),
                    Some("assertion_line") => assertion_line = value.as_u64().map(|x| x as u32),
                    Some("description") => description = value.as_str().map(Into::into),
                    Some("expression") => expression = value.as_str().map(Into::into),
                    Some("info") if !value.is_nil() => info = Some(value),
                    Some("input_file") => input_file = value.as_str().map(Into::into),
                    Some("snapshot_kind") => {
                        snapshot_type = match value.as_str() {
                            Some("binary") => TmpSnapshotKind::Binary,
                            _ => TmpSnapshotKind::Text,
                        }
                    }
                    Some("extension") => {
                        extension = value.as_str().map(Into::into);
                    }
                    _ => {}
                }
            }

            Ok(MetaData {
                source,
                assertion_line,
                description,
                expression,
                info,
                input_file,
                snapshot_kind: match snapshot_type {
                    TmpSnapshotKind::Text => SnapshotKind::Text,
                    TmpSnapshotKind::Binary => SnapshotKind::Binary {
                        extension: extension.ok_or(content::Error::MissingField)?,
                    },
                },
            })
        } else {
            Err(content::Error::UnexpectedDataType.into())
        }
    }

    fn as_content(&self) -> Content {
        let mut fields = Vec::new();
        if let Some(source) = self.source.as_deref() {
            fields.push(("source", Content::from(source)));
        }
        if let Some(line) = self.assertion_line {
            fields.push(("assertion_line", Content::from(line)));
        }
        if let Some(description) = self.description.as_deref() {
            fields.push(("description", Content::from(description)));
        }
        if let Some(expression) = self.expression.as_deref() {
            fields.push(("expression", Content::from(expression)));
        }
        if let Some(info) = &self.info {
            fields.push(("info", info.to_owned()));
        }
        if let Some(input_file) = self.input_file.as_deref() {
            fields.push(("input_file", Content::from(input_file)));
        }

        match self.snapshot_kind {
            SnapshotKind::Text => {}
            SnapshotKind::Binary { ref extension } => {
                fields.push(("extension", Content::from(extension.clone())));
                fields.push(("snapshot_kind", Content::from("binary")));
            }
        }

        Content::Struct("MetaData", fields)
    }

    /// Trims the metadata of fields that we don't save to `.snap` files (those
    /// we only use for display while reviewing)
    pub(crate) fn trim_for_persistence(&self) -> Cow<'_, MetaData> {
        // TODO: in order for `--require-full-match` to work on inline snapshots
        // without cargo-insta, we need to trim all fields if there's an inline
        // snapshot. But we don't know that from here (notably
        // `self.input_file.is_none()` is not a correct approach). Given that
        // `--require-full-match` is experimental and we're working on making
        // inline & file snapshots more coherent, I'm leaving this as is for
        // now.
        if self.assertion_line.is_some() {
            let mut rv = self.clone();
            rv.assertion_line = None;
            Cow::Owned(rv)
        } else {
            Cow::Borrowed(self)
        }
    }
}

#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum TextSnapshotKind {
    Inline,
    File,
}

/// A helper to work with file snapshots.
#[derive(Debug, Clone)]
pub struct Snapshot {
    module_name: String,
    snapshot_name: Option<String>,
    pub(crate) metadata: MetaData,
    snapshot: SnapshotContents,
}

impl Snapshot {
    /// Loads a snapshot from a file.
    pub fn from_file(p: &Path) -> Result<Snapshot, Box<dyn Error>> {
        let mut f = BufReader::new(fs::File::open(p)?);
        let mut buf = String::new();

        f.read_line(&mut buf)?;

        // yaml format
        let metadata = if buf.trim_end() == "---" {
            loop {
                let read = f.read_line(&mut buf)?;
                if read == 0 {
                    break;
                }
                if buf[buf.len() - read..].trim_end() == "---" {
                    buf.truncate(buf.len() - read);
                    break;
                }
            }
            let content = yaml::parse_str(&buf, p)?;
            MetaData::from_content(content)?
        // legacy format
        // (but not viable to move into `match_legacy` given it's more than
        // just the snapshot value itself...)
        } else {
            let mut rv = MetaData::default();
            loop {
                buf.clear();
                let read = f.read_line(&mut buf)?;
                if read == 0 || buf.trim_end().is_empty() {
                    buf.truncate(buf.len() - read);
                    break;
                }
                let mut iter = buf.splitn(2, ':');
                if let Some(key) = iter.next() {
                    if let Some(value) = iter.next() {
                        let value = value.trim();
                        match key.to_lowercase().as_str() {
                            "expression" => rv.expression = Some(value.to_string()),
                            "source" => rv.source = Some(value.into()),
                            _ => {}
                        }
                    }
                }
            }
            elog!("A snapshot uses a legacy snapshot format; please update it to the new format with `cargo insta test --force-update-snapshots --accept`.\nSnapshot is at: {}", p.to_string_lossy());
            rv
        };

        let contents = match metadata.snapshot_kind {
            SnapshotKind::Text => {
                buf.clear();
                for (idx, line) in f.lines().enumerate() {
                    let line = line?;
                    if idx > 0 {
                        buf.push('\n');
                    }
                    buf.push_str(&line);
                }

                TextSnapshotContents {
                    contents: buf,
                    kind: TextSnapshotKind::File,
                }
                .into()
            }
            SnapshotKind::Binary { ref extension } => {
                let path = build_binary_path(extension, p);
                // A missing sidecar data file is a valid state (see
                // `SnapshotContents::Binary`); we represent it as `None` rather than
                // erroring. A present-but-unreadable file is still a real error.
                match fs::read(path) {
                    Ok(contents) => SnapshotContents::Binary(Some(Rc::new(contents))),
                    Err(e) if e.kind() == io::ErrorKind::NotFound => SnapshotContents::Binary(None),
                    Err(e) => return Err(e.into()),
                }
            }
        };

        let (snapshot_name, module_name) = names_of_path(p);

        Ok(Snapshot::from_components(
            module_name,
            Some(snapshot_name),
            metadata,
            contents,
        ))
    }

    pub(crate) fn from_components(
        module_name: String,
        snapshot_name: Option<String>,
        metadata: MetaData,
        snapshot: SnapshotContents,
    ) -> Snapshot {
        Snapshot {
            module_name,
            snapshot_name,
            metadata,
            snapshot,
        }
    }

    #[cfg(feature = "_cargo_insta_internal")]
    fn from_content(content: Content, kind: TextSnapshotKind) -> Result<Snapshot, Box<dyn Error>> {
        if let Content::Map(map) = content {
            let mut module_name = None;
            let mut snapshot_name = None;
            let mut metadata = None;
            let mut snapshot = None;

            for (key, value) in map.into_iter() {
                match key.as_str() {
                    Some("module_name") => module_name = value.as_str().map(|x| x.to_string()),
                    Some("snapshot_name") => snapshot_name = value.as_str().map(|x| x.to_string()),
                    Some("metadata") => metadata = Some(MetaData::from_content(value)?),
                    Some("snapshot") => {
                        snapshot = Some(
                            TextSnapshotContents {
                                contents: value
                                    .as_str()
                                    .ok_or(content::Error::UnexpectedDataType)?
                                    .to_string(),
                                kind,
                            }
                            .into(),
                        );
                    }
                    _ => {}
                }
            }

            Ok(Snapshot {
                module_name: module_name.ok_or(content::Error::MissingField)?,
                snapshot_name,
                metadata: metadata.ok_or(content::Error::MissingField)?,
                snapshot: snapshot.ok_or(content::Error::MissingField)?,
            })
        } else {
            Err(content::Error::UnexpectedDataType.into())
        }
    }

    fn as_content(&self) -> Content {
        let mut fields = vec![("module_name", Content::from(self.module_name.as_str()))];
        // Note this is currently never used, since this method is only used for
        // inline snapshots
        if let Some(name) = self.snapshot_name.as_deref() {
            fields.push(("snapshot_name", Content::from(name)));
        }
        fields.push(("metadata", self.metadata.as_content()));

        if let SnapshotContents::Text(ref content) = self.snapshot {
            fields.push(("snapshot", Content::from(content.to_string())));
        }

        Content::Struct("Content", fields)
    }

    /// Returns the module name.
    pub fn module_name(&self) -> &str {
        &self.module_name
    }

    /// Returns the snapshot name.
    pub fn snapshot_name(&self) -> Option<&str> {
        self.snapshot_name.as_deref()
    }

    /// The metadata in the snapshot.
    pub fn metadata(&self) -> &MetaData {
        &self.metadata
    }

    /// The snapshot contents
    pub fn contents(&self) -> &SnapshotContents {
        &self.snapshot
    }

    /// Returns the text contents if this is a text snapshot.
    pub fn as_text(&self) -> Option<&TextSnapshotContents> {
        self.snapshot.as_text()
    }

    fn serialize_snapshot(&self, md: &MetaData) -> String {
        let mut buf = yaml::to_string(&md.as_content());
        buf.push_str("---\n");

        if let SnapshotContents::Text(ref contents) = self.snapshot {
            buf.push_str(&contents.to_string());
            buf.push('\n');
        }

        buf
    }

    // We take `md` as an argument here because the calling methods want to
    // adjust it; e.g. removing volatile fields when writing to the final
    // `.snap` file.
    fn save_with_metadata(&self, path: &Path, md: &MetaData) -> Result<(), Box<dyn Error>> {
        if let Some(folder) = path.parent() {
            fs::create_dir_all(folder)?;
        }

        let serialized_snapshot = self.serialize_snapshot(md);
        fs::write(path, serialized_snapshot)
            .map_err(|e| content::Error::FileIo(e, path.to_path_buf()))?;

        if let SnapshotContents::Binary(ref contents) = self.snapshot {
            // Only snapshots we just produced are ever saved, and those always carry
            // their data. An absent payload (loaded from disk with a missing sidecar)
            // has nothing to write.
            let contents = contents
                .as_ref()
                .expect("cannot save a binary snapshot with an absent data file");
            fs::write(self.build_binary_path(path).unwrap(), &**contents)
                .map_err(|e| content::Error::FileIo(e, path.to_path_buf()))?;
        }

        Ok(())
    }

    pub fn build_binary_path(&self, path: impl Into<PathBuf>) -> Option<PathBuf> {
        if let SnapshotKind::Binary { ref extension } = self.metadata.snapshot_kind {
            Some(build_binary_path(extension, path))
        } else {
            None
        }
    }

    /// Saves the snapshot.
    #[doc(hidden)]
    pub fn save(&self, path: &Path) -> Result<(), Box<dyn Error>> {
        self.save_with_metadata(path, &self.metadata.trim_for_persistence())
    }

    /// Same as [`Self::save`] but instead of writing a normal snapshot file this will write
    /// a `.snap.new` file with additional information.
    ///
    /// The path of the new snapshot file is returned.
    pub(crate) fn save_new(&self, path: &Path) -> Result<PathBuf, Box<dyn Error>> {
        // TODO: should we be the actual extension here rather than defaulting
        // to the standard `.snap`?
        let new_path = path.to_path_buf().with_extension("snap.new");
        self.save_with_metadata(&new_path, &self.metadata)?;
        Ok(new_path)
    }
}

/// The contents of a Snapshot
#[derive(Debug, Clone)]
pub enum SnapshotContents {
    Text(TextSnapshotContents),

    // `None` represents an absent payload: the `.snap` metadata exists but the
    // sidecar data file does not. A gitignored data file is never committed, so
    // it is absent on a fresh checkout even though the metadata is tracked. This
    // is a valid state for an old snapshot loaded from disk; new snapshots always
    // carry their data as `Some`.
    //
    // This is in an `Rc` because we need to be able to clone this struct cheaply and the contents
    // of the `Vec` could be rather large. The reason it's not an `Rc<[u8]>` is because creating one
    // of those would require re-allocating because of the additional size needed for the reference
    // count.
    Binary(Option<Rc<Vec<u8>>>),
}

// Could be Cow, but I think limited savings
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct TextSnapshotContents {
    contents: String,
    pub kind: TextSnapshotKind,
}

impl From<TextSnapshotContents> for SnapshotContents {
    fn from(value: TextSnapshotContents) -> Self {
        SnapshotContents::Text(value)
    }
}

impl SnapshotContents {
    pub fn is_binary(&self) -> bool {
        matches!(self, SnapshotContents::Binary(_))
    }

    /// Returns the text contents if this is a text snapshot.
    pub fn as_text(&self) -> Option<&TextSnapshotContents> {
        match self {
            SnapshotContents::Text(t) => Some(t),
            SnapshotContents::Binary(_) => None,
        }
    }
}

impl TextSnapshotContents {
    pub fn new(contents: String, kind: TextSnapshotKind) -> TextSnapshotContents {
        // We could store a normalized version of the string as part of `new`;
        // it would avoid allocating a new `String` when we get the normalized
        // versions, which we may do a few times. (We want to store the
        // unnormalized version because it allows us to use `matches_fully`.)
        TextSnapshotContents { contents, kind }
    }

    /// Matches another snapshot without any normalization
    pub fn matches_fully(&self, other: &TextSnapshotContents) -> bool {
        self.contents == other.contents
    }

    /// Snapshot matches based on the latest format.
    pub fn matches_latest(&self, other: &Self) -> bool {
        self.to_string() == other.to_string()
    }

    pub fn matches_legacy(&self, other: &Self) -> bool {
        fn as_str_legacy(sc: &TextSnapshotContents) -> String {
            // First do the standard normalization
            let out = sc.to_string();
            // Legacy snapshots trim newlines at the start.
            let out = out.trim_start_matches(['\r', '\n']);
            // Legacy inline snapshots have `---` at the start, so this strips that if
            // it exists.
            let out = match out.strip_prefix("---\n") {
                Some(old_snapshot) => old_snapshot,
                None => out,
            };
            match sc.kind {
                TextSnapshotKind::Inline => {
                    let out = legacy_inline_normalize(out);
                    // Handle old multiline format where single-line content was stored
                    // with code indentation (e.g., @r"\n    content\n    "). After
                    // from_inline_literal processing, this becomes "    content\n    "
                    // with leading spaces from source code indentation.
                    //
                    // We detect this by checking:
                    // 1. Raw contents contain a newline (came from multiline literal)
                    // 2. After trimming, it's effectively single-line (the legacy pattern)
                    //
                    // This distinguishes:
                    // - Legacy single-line in multiline: @r"\n    X\n    " → trim
                    // - Modern single-line: @"    X" → don't trim (intentional spaces)
                    // - True multiline: @r"\n    A\n    B\n" → don't trim (>1 line)
                    //
                    // See: https://github.com/mitsuhiko/insta/pull/819#issuecomment-3583709431
                    let is_legacy_single_line_in_multiline =
                        sc.contents.contains('\n') && sc.contents.trim_end().lines().count() <= 1;
                    if is_legacy_single_line_in_multiline {
                        out.trim_start().to_string()
                    } else {
                        out
                    }
                }
                TextSnapshotKind::File => out.to_string(),
            }
        }
        as_str_legacy(self) == as_str_legacy(other)
    }

    /// Convert a literal snapshot value (i.e. the string inside the quotes,
    /// from a rust file) to the value we retain in the struct. This is a small
    /// change to the value: we remove the leading newline and coerce newlines
    /// to `\n`. Otherwise, the value is retained unnormalized (generally we
    /// want to retain unnormalized values so we can run `matches_fully` on
    /// them)
    /// Returns true if the string contains control characters that require
    /// escaped format (using Rust's `{:?}` Debug formatting).
    ///
    /// We exclude `\n`, `\t`, and `\x1b` from triggering escaped format:
    /// - `\n` (newline): Can appear literally in block format strings
    /// - `\t` (tab): Can appear literally in strings without escaping
    /// - `\x1b` (ESC): Used for ANSI terminal colors; users prefer seeing
    ///   the actual escape sequences rather than double-escaped output
    ///
    /// All other control characters (like `\r`, `\0`) trigger escaped format.
    fn needs_escaped_format(contents: &str) -> bool {
        contents
            .chars()
            .any(|c| c.is_control() && !['\n', '\t', '\x1b'].contains(&c))
    }

    pub(crate) fn from_inline_literal(contents: &str) -> Self {
        // If it's a single line string, then we don't do anything.
        if contents.trim_end().lines().count() <= 1 {
            return Self::new(contents.trim_end().to_string(), TextSnapshotKind::Inline);
        }

        // Escaped format doesn't use a formatting newline, so return as-is.
        // Block format uses a formatting newline that we strip below.
        if Self::needs_escaped_format(contents) {
            return Self::new(contents.trim_end().to_string(), TextSnapshotKind::Inline);
        }

        // If it's multiline block format, we trim the first line, which should be empty.
        // (Possibly in the future we'll do the same for the final line too)
        let lines = contents.lines().collect::<Vec<&str>>();
        let (first, remainder) = lines.split_first().unwrap();
        let snapshot = {
            // If the first isn't empty, something is up — include the first line
            // and print a warning.
            if first != &"" {
                elog!("{} {}{}{}\n{}",style("Multiline inline snapshot values should start and end with a newline.").yellow().bold()," The current value will fail to match in the future. Run `cargo insta test --force-update-snapshots` to rewrite snapshots. The existing value's first line is `", first, "`. Full value:", contents);
                once(first)
                    .chain(remainder.iter())
                    .cloned()
                    .collect::<Vec<&str>>()
                    .join("\n")
            } else {
                remainder.join("\n")
            }
        };
        Self::new(snapshot, TextSnapshotKind::Inline)
    }

    fn normalize(&self) -> String {
        let kind_specific_normalization = match self.kind {
            TextSnapshotKind::Inline => normalize_inline(&self.contents),
            TextSnapshotKind::File => self.contents.clone(),
        };
        // Then this we do for both kinds
        let out = kind_specific_normalization.trim_end();
        // Normalize Windows CRLF to LF. We intentionally do NOT normalize
        // standalone \r (old Mac Classic line endings) to \n here, as standalone
        // \r in snapshot content is more likely to be data (terminal control
        // codes, binary output) than a line ending. If this assumption proves
        // wrong, we could add: .replace('\r', "\n")
        out.replace("\r\n", "\n")
    }

    /// Returns the string literal, including `#` delimiters, to insert into a
    /// Rust source file.
    pub fn to_inline(&self, indentation: &str) -> String {
        let contents = self.normalize();
        let mut out = String::new();

        let needs_escaping = Self::needs_escaped_format(&contents);

        // We prefer raw strings for strings containing a quote or an escape
        // character, as these would require escaping in regular strings.
        // We can't use raw strings for control characters that need escaping.
        // We don't use raw strings just for newlines, as they can appear
        // literally in regular strings (avoids clippy::needless_raw_strings).
        if !needs_escaping && contents.contains(['\\', '"']) {
            out.push('r');
        }

        let delimiter = "#".repeat(required_hashes(&contents));

        out.push_str(&delimiter);

        // If escaping is needed, use Rust's Debug formatting for escape sequences.
        // We don't attempt block mode for these (though not impossible to do so).
        if needs_escaping {
            out.push_str(format!("{contents:?}").as_str());
        } else {
            out.push('"');
            // if we have more than one line we want to change into the block
            // representation mode
            if contents.contains('\n') {
                out.extend(
                    contents
                        .lines()
                        // Adds an additional newline at the start of multiline
                        // string (not sure this is the clearest way of representing
                        // it, but it works...)
                        .map(|l| {
                            format!(
                                "\n{i}{l}",
                                i = if l.is_empty() { "" } else { indentation },
                                l = l
                            )
                        })
                        // `lines` removes the final line ending — add back. Include
                        // indentation so the closing delimited aligns with the full string.
                        .chain(Some(format!("\n{indentation}"))),
                );
            } else {
                out.push_str(contents.as_str());
            }
            out.push('"');
        }

        out.push_str(&delimiter);
        out
    }
}

impl fmt::Display for TextSnapshotContents {
    /// Returns the snapshot contents as a normalized string (for example,
    /// removing surrounding whitespace)
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.normalize())
    }
}

impl PartialEq for SnapshotContents {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (SnapshotContents::Text(this), SnapshotContents::Text(other)) => {
                // Ideally match on current rules, but otherwise fall back to legacy rules
                if this.matches_latest(other) {
                    true
                } else if this.matches_legacy(other) {
                    elog!("{} {}\n{}",style("Snapshot test passes but the existing value is in a legacy format. Please run `cargo insta test --force-update-snapshots` to update to a newer format.").yellow().bold(),"Snapshot contents:", this.to_string());
                    true
                } else {
                    false
                }
            }
            // Two present payloads compare by bytes. A present payload never equals an
            // absent one, so an old `None` (missing data file) vs a new `Some(..)` is
            // unequal — i.e. treated as changed, which is what we want. Two absent
            // payloads compare equal (there's nothing to distinguish them).
            (SnapshotContents::Binary(this), SnapshotContents::Binary(other)) => this == other,
            _ => false,
        }
    }
}

fn build_binary_path(extension: &str, path: impl Into<PathBuf>) -> PathBuf {
    let path = path.into();
    let mut new_extension = path.extension().unwrap().to_os_string();
    new_extension.push(".");
    new_extension.push(extension);

    path.with_extension(new_extension)
}

/// The number of `#` we need to surround a raw string literal with.
fn required_hashes(text: &str) -> usize {
    text.split('"')
        .skip(1) // Skip the first part which is before the first quote
        .map(|s| s.chars().take_while(|&c| c == '#').count() + 1)
        .max()
        .unwrap_or_default()
}

#[test]
fn test_required_hashes() {
    assert_snapshot!(required_hashes(""), @"0");
    assert_snapshot!(required_hashes("Hello, world!"), @"0");
    assert_snapshot!(required_hashes("\"\""), @"1");
    assert_snapshot!(required_hashes("##"), @"0");
    assert_snapshot!(required_hashes("\"#\"#"), @"2");
    assert_snapshot!(required_hashes(r##""#"##), @"2");
    assert_snapshot!(required_hashes(r######"foo ""##### bar "###" baz"######), @"6");
    assert_snapshot!(required_hashes("\"\"\""), @"1");
    assert_snapshot!(required_hashes("####"), @"0");
    assert_snapshot!(required_hashes(r###"\"\"##\"\""###), @"3");
    assert_snapshot!(required_hashes(r###"r"#"Raw string"#""###), @"2");
}

fn leading_space(value: &str) -> String {
    value
        .chars()
        // Only consider horizontal whitespace (space and tab) as indentation.
        // Other whitespace like \r should not be stripped as indentation.
        .take_while(|x| *x == ' ' || *x == '\t')
        .collect::<String>()
}

fn min_indentation(snapshot: &str) -> String {
    let lines = snapshot.trim_end().lines();

    lines
        .filter(|l| !l.is_empty())
        .map(leading_space)
        .min_by(|a, b| a.len().cmp(&b.len()))
        .unwrap_or("".into())
}

/// Normalize snapshot value, which we apply to both generated and literal
/// snapshots. Remove excess indentation, excess ending whitespace and coerce
/// newlines to `\n`.
fn normalize_inline(snapshot: &str) -> String {
    // If it's a single line string, then we don't do anything.
    if snapshot.trim_end().lines().count() <= 1 {
        return snapshot.trim_end().to_string();
    }

    let indentation = min_indentation(snapshot);
    snapshot
        .lines()
        .map(|l| l.get(indentation.len()..).unwrap_or(""))
        .collect::<Vec<&str>>()
        .join("\n")
}

#[test]
fn test_normalize_inline_snapshot() {
    fn normalized_of_literal(snapshot: &str) -> String {
        normalize_inline(&TextSnapshotContents::from_inline_literal(snapshot).contents)
    }

    use similar_asserts::assert_eq;
    // here we do exact matching (rather than `assert_snapshot`) to ensure we're
    // not incorporating the modifications that insta itself makes

    assert_eq!(
        normalized_of_literal(
            "
   1
   2
"
        ),
        "1
2"
    );

    assert_eq!(
        normalized_of_literal(
            "
            1
    2
    "
        ),
        "        1
2
"
    );

    assert_eq!(
        normalized_of_literal(
            "
            1
            2
    "
        ),
        "1
2
"
    );

    assert_eq!(
        normalized_of_literal(
            "
   1
   2
"
        ),
        "1
2"
    );

    assert_eq!(
        normalized_of_literal(
            "
        a
    "
        ),
        "        a"
    );

    assert_eq!(normalized_of_literal(""), "");

    assert_eq!(
        normalized_of_literal(
            "
    a
    b
c
    "
        ),
        "    a
    b
c
    "
    );

    assert_eq!(
        normalized_of_literal(
            "
a
    "
        ),
        "a"
    );

    // This is a bit of a weird case, but because it's not a true multiline
    // (which requires an opening and closing newline), we don't trim the
    // indentation. Not terrible if this needs to change. The next test shows
    // how a real multiline string is handled.
    assert_eq!(
        normalized_of_literal(
            "
    a"
        ),
        "    a"
    );

    // This test will pass but raise a warning, so we comment it out for the moment.
    // assert_eq!(
    //     normalized_of_literal(
    //         "a
    //   a"
    //     ),
    //     "a
    //   a"
    // );
}

/// Extracts the module and snapshot name from a snapshot path
fn names_of_path(path: &Path) -> (String, String) {
    // The final part of the snapshot file name is the test name; the
    // initial parts are the module name
    let parts: Vec<&str> = path
        .file_stem()
        .unwrap()
        .to_str()
        .unwrap_or("")
        .rsplitn(2, "__")
        .collect();

    match parts.as_slice() {
        [snapshot_name, module_name] => (snapshot_name.to_string(), module_name.to_string()),
        [snapshot_name] => (snapshot_name.to_string(), String::new()),
        _ => (String::new(), "<unknown>".to_string()),
    }
}

#[test]
fn test_names_of_path() {
    assert_debug_snapshot!(
        names_of_path(Path::new("/src/snapshots/insta_tests__tests__name_foo.snap")), @r#"
    (
        "name_foo",
        "insta_tests__tests",
    )
    "#
    );
    assert_debug_snapshot!(
        names_of_path(Path::new("/src/snapshots/name_foo.snap")), @r#"
    (
        "name_foo",
        "",
    )
    "#
    );
    assert_debug_snapshot!(
        names_of_path(Path::new("foo/src/snapshots/go1.20.5.snap")), @r#"
    (
        "go1.20.5",
        "",
    )
    "#
    );
}

/// legacy format - retain so old snapshots still work
fn legacy_inline_normalize(frozen_value: &str) -> String {
    if !frozen_value.trim_start().starts_with('⋮') {
        return frozen_value.to_string();
    }
    let mut buf = String::new();
    let mut line_iter = frozen_value.lines();
    let mut indentation = 0;

    for line in &mut line_iter {
        let line_trimmed = line.trim_start();
        if line_trimmed.is_empty() {
            continue;
        }
        indentation = line.len() - line_trimmed.len();
        // 3 because '⋮' is three utf-8 bytes long
        buf.push_str(&line_trimmed[3..]);
        buf.push('\n');
        break;
    }

    for line in &mut line_iter {
        if let Some(prefix) = line.get(..indentation) {
            if !prefix.trim().is_empty() {
                return "".to_string();
            }
        }
        if let Some(remainder) = line.get(indentation..) {
            if let Some(rest) = remainder.strip_prefix('⋮') {
                buf.push_str(rest);
                buf.push('\n');
            } else if remainder.trim().is_empty() {
                continue;
            } else {
                return "".to_string();
            }
        }
    }

    buf.trim_end().to_string()
}

#[test]
fn test_snapshot_contents_to_inline() {
    use similar_asserts::assert_eq;
    let snapshot_contents =
        TextSnapshotContents::new("testing".to_string(), TextSnapshotKind::Inline);
    assert_eq!(snapshot_contents.to_inline(""), r#""testing""#);

    assert_eq!(
        TextSnapshotContents::new("\na\nb".to_string(), TextSnapshotKind::Inline).to_inline(""),
        r##""

a
b
""##
    );

    assert_eq!(
        TextSnapshotContents::new("a\nb".to_string(), TextSnapshotKind::Inline).to_inline("    "),
        r##""
    a
    b
    ""##
    );

    assert_eq!(
        TextSnapshotContents::new("\n    a\n    b".to_string(), TextSnapshotKind::Inline)
            .to_inline(""),
        r##""

a
b
""##
    );

    assert_eq!(
        TextSnapshotContents::new("\na\n\nb".to_string(), TextSnapshotKind::Inline)
            .to_inline("    "),
        r##""

    a

    b
    ""##
    );

    assert_eq!(
        TextSnapshotContents::new(
            "ab
    "
            .to_string(),
            TextSnapshotKind::Inline
        )
        .to_inline(""),
        r#""ab""#
    );

    assert_eq!(
        TextSnapshotContents::new(
            "    ab
    "
            .to_string(),
            TextSnapshotKind::Inline
        )
        .to_inline(""),
        r##""    ab""##
    );

    assert_eq!(
        TextSnapshotContents::new("\n    ab\n".to_string(), TextSnapshotKind::Inline).to_inline(""),
        r##""

ab
""##
    );

    assert_eq!(
        TextSnapshotContents::new("ab".to_string(), TextSnapshotKind::Inline).to_inline(""),
        r#""ab""#
    );

    // Test control and special characters
    assert_eq!(
        TextSnapshotContents::new("a\tb".to_string(), TextSnapshotKind::Inline).to_inline(""),
        r##""a	b""##
    );

    assert_eq!(
        TextSnapshotContents::new("a\t\nb".to_string(), TextSnapshotKind::Inline).to_inline(""),
        "\"
a\t
b
\""
    );

    assert_eq!(
        TextSnapshotContents::new("a\rb".to_string(), TextSnapshotKind::Inline).to_inline(""),
        r##""a\rb""##
    );

    // Issue #865: carriage return at start of line should be preserved.
    // Escaped format doesn't add a formatting newline (unlike block format).
    assert_eq!(
        TextSnapshotContents::new("\n\r foo  bar".to_string(), TextSnapshotKind::Inline)
            .to_inline(""),
        r##""\n\r foo  bar""##
    );

    assert_eq!(
        TextSnapshotContents::new("a\0b".to_string(), TextSnapshotKind::Inline).to_inline(""),
        // Nul byte is printed as `\0` in Rust string literals
        r##""a\0b""##
    );

    assert_eq!(
        TextSnapshotContents::new("a\u{FFFD}b".to_string(), TextSnapshotKind::Inline).to_inline(""),
        // Replacement character is returned as the character in literals
        r##""a�b""##
    );

    // CRLF edge cases - CRLF normalizes to LF, but standalone CR is preserved
    assert_eq!(
        TextSnapshotContents::new("hello\r\nworld".to_string(), TextSnapshotKind::Inline)
            .to_inline("    "),
        // CRLF becomes LF after normalization, so uses block format
        r##""
    hello
    world
    ""##
    );

    assert_eq!(
        TextSnapshotContents::new("\r\nhello".to_string(), TextSnapshotKind::Inline).to_inline(""),
        // Leading CRLF normalizes to leading LF
        r##""

hello
""##
    );

    assert_eq!(
        TextSnapshotContents::new("hello\r\n".to_string(), TextSnapshotKind::Inline).to_inline(""),
        // Trailing CRLF is trimmed (like all trailing whitespace)
        r##""hello""##
    );

    // LFCR (not CRLF) - the CR is standalone and triggers escaped format
    // Escaped format doesn't add a formatting newline (unlike block format)
    assert_eq!(
        TextSnapshotContents::new("hello\n\rworld".to_string(), TextSnapshotKind::Inline)
            .to_inline(""),
        r##""hello\n\rworld""##
    );

    // Mixed CR and CRLF
    assert_eq!(
        TextSnapshotContents::new("a\rb\r\nc".to_string(), TextSnapshotKind::Inline).to_inline(""),
        // After CRLF normalization: "a\rb\nc" - has CR, uses escaped format
        r##""a\rb\nc""##
    );
}

/// Test that escaped format content roundtrips correctly through `from_inline_literal`.
/// Issue #865: content with control chars like \r should not lose its leading newline.
#[test]
fn test_escaped_format_preserves_content() {
    // Escaped format: from_inline_literal receives the value as-is (no formatting newline)
    // Block format: from_inline_literal receives the value with a leading formatting newline

    // The bug was: "\n\r foo" would lose its leading \n because from_inline_literal
    // stripped it, thinking it was a formatting newline from block format.

    // Escaped format cases (has control chars like \r)
    assert_eq!(
        TextSnapshotContents::from_inline_literal("\n\r foo").contents,
        "\n\r foo"
    );
    assert_eq!(
        TextSnapshotContents::from_inline_literal("a\rb").contents,
        "a\rb"
    );

    // Block format cases (no control chars) - leading newline IS a formatting newline
    // (trailing newline is trimmed by from_inline_literal)
    assert_eq!(
        TextSnapshotContents::from_inline_literal("\nfoo\nbar\n").contents,
        "foo\nbar"
    );
}

#[test]
fn test_snapshot_contents_hashes() {
    assert_eq!(
        TextSnapshotContents::new("a###b".to_string(), TextSnapshotKind::Inline).to_inline(""),
        r#""a###b""#
    );

    assert_eq!(
        TextSnapshotContents::new("a\n\\###b".to_string(), TextSnapshotKind::Inline).to_inline(""),
        r#####"r"
a
\###b
""#####
    );
}

#[test]
fn test_snapshot_contents_as_text() {
    let text = SnapshotContents::Text(TextSnapshotContents::new(
        "hello".to_string(),
        TextSnapshotKind::Inline,
    ));
    assert!(text.as_text().is_some());
    assert_eq!(text.as_text().unwrap().to_string(), "hello");

    let binary = SnapshotContents::Binary(Some(Rc::new(vec![1, 2, 3])));
    assert!(binary.as_text().is_none());
}

#[test]
fn test_min_indentation() {
    use similar_asserts::assert_eq;
    assert_eq!(
        min_indentation(
            "
   1
   2
   ",
        ),
        "   ".to_string()
    );

    assert_eq!(
        min_indentation(
            "
            1
    2"
        ),
        "    ".to_string()
    );

    assert_eq!(
        min_indentation(
            "
            1
            2
    "
        ),
        "            ".to_string()
    );

    assert_eq!(
        min_indentation(
            "
   1
   2
"
        ),
        "   ".to_string()
    );

    assert_eq!(
        min_indentation(
            "
        a
    "
        ),
        "        ".to_string()
    );

    assert_eq!(min_indentation(""), "".to_string());

    assert_eq!(
        min_indentation(
            "
    a
    b
c
    "
        ),
        "".to_string()
    );

    assert_eq!(
        min_indentation(
            "
a
    "
        ),
        "".to_string()
    );

    assert_eq!(
        min_indentation(
            "
    a"
        ),
        "    ".to_string()
    );

    assert_eq!(
        min_indentation(
            "a
  a"
        ),
        "".to_string()
    );

    assert_eq!(
        normalize_inline(
            "
			1
	2"
        ),
        "
		1
2"
    );

    assert_eq!(
        normalize_inline(
            "
	  	  1
	  	  2
    "
        ),
        "
1
2
"
    );
}

#[test]
fn test_min_indentation_additional() {
    use similar_asserts::assert_eq;

    let t = "
   1
   2
";
    assert_eq!(min_indentation(t), "   ".to_string());

    let t = "
        a
    ";
    assert_eq!(min_indentation(t), "        ".to_string());

    let t = "";
    assert_eq!(min_indentation(t), "".to_string());

    let t = "
    a
    b
c
    ";
    assert_eq!(min_indentation(t), "".to_string());

    let t = "
a";
    assert_eq!(min_indentation(t), "".to_string());

    let t = "
    a";
    assert_eq!(min_indentation(t), "    ".to_string());

    let t = "a
  a";
    assert_eq!(min_indentation(t), "".to_string());

    let t = "
 	1
 	2
    ";
    assert_eq!(min_indentation(t), " 	".to_string());

    let t = "
  	  	  	1
  	2";
    assert_eq!(min_indentation(t), "  	".to_string());

    let t = "
			1
	2";
    assert_eq!(min_indentation(t), "	".to_string());
}

#[test]
fn test_inline_snapshot_value_newline() {
    // https://github.com/mitsuhiko/insta/issues/39
    assert_eq!(normalize_inline("\n"), "");
}

#[test]
fn test_parse_yaml_error() {
    use std::env::temp_dir;
    let mut temp = temp_dir();
    temp.push("bad.yaml");
    let mut f = fs::File::create(temp.clone()).unwrap();

    let invalid = "---
    This is invalid yaml:
     {
        {
    ---
    ";

    f.write_all(invalid.as_bytes()).unwrap();

    let error = format!("{}", Snapshot::from_file(temp.as_path()).unwrap_err());
    assert!(error.contains("Failed parsing the YAML from"));
    assert!(error.contains("bad.yaml"));
}

/// Check that snapshots don't take ownership of the value
#[test]
fn test_ownership() {
    // Range is non-copy
    use std::ops::Range;
    let r = Range { start: 0, end: 10 };
    assert_debug_snapshot!(r, @"0..10");
    assert_debug_snapshot!(r, @"0..10");
}

#[test]
fn test_empty_lines() {
    assert_snapshot!("single line should fit on a single line", @"single line should fit on a single line");
    assert_snapshot!("single line should fit on a single line, even if it's really really really really really really really really really long", @"single line should fit on a single line, even if it's really really really really really really really really really long");

    assert_snapshot!("multiline content starting on first line

    final line
    ", @"
    multiline content starting on first line

        final line
    ");

    assert_snapshot!("
    multiline content starting on second line

    final line
    ", @"

    multiline content starting on second line

    final line
    ");
}
