use std::fmt::Write as _;

use crate::error::{EmitError, EmitResult};
use crate::value::Value;
use memchr::memmem;
use saphyr::{ScalarOwned, YamlEmitter};
use saphyr_parser::ScalarStyle;

/// Configuration for YAML emission.
///
/// Controls formatting, style, and output options when serializing YAML.
#[derive(Debug, Clone)]
pub struct EmitterConfig {
    /// Indentation width in spaces (default: 2).
    ///
    /// Controls the number of spaces used for each indentation level.
    /// Valid range: 1-9 (values outside this range will be clamped).
    ///
    /// Note: saphyr currently uses fixed 2-space indentation.
    /// This parameter is accepted for `PyYAML` API compatibility but
    /// may require post-processing to fully support custom values.
    pub indent: usize,

    /// Maximum line width for wrapping (default: 80).
    ///
    /// When lines exceed this width, the emitter will attempt to wrap them.
    /// Valid range: 20-1000 (values outside this range will be clamped).
    ///
    /// Note: saphyr has limited control over line wrapping.
    /// This parameter is accepted for `PyYAML` API compatibility.
    pub width: usize,

    /// Default flow style for collections (default: None).
    ///
    /// - `None`: Use block style (multi-line)
    /// - `Some(true)`: Force flow style (inline: `[...]`, `{...}`)
    /// - `Some(false)`: Force block style (explicit)
    pub default_flow_style: Option<bool>,

    /// Add explicit document start marker `---` (default: false).
    ///
    /// When true, prepends `---\n` to the output.
    pub explicit_start: bool,

    /// Enable compact inline notation (default: true).
    ///
    /// Controls whether saphyr uses compact notation for
    /// inline sequences and mappings.
    pub compact: bool,

    /// Render multiline strings in literal style (default: false).
    ///
    /// When true, strings containing newlines will be rendered
    /// using literal block scalar notation (`|`).
    pub multiline_strings: bool,
}

impl Default for EmitterConfig {
    fn default() -> Self {
        Self {
            indent: 2,
            width: 80,
            default_flow_style: None,
            explicit_start: false,
            compact: true,
            multiline_strings: false,
        }
    }
}

impl EmitterConfig {
    /// Create a new emitter configuration with default values.
    pub fn new() -> Self {
        Self::default()
    }

    /// Set indentation width (clamped to 1-9).
    #[must_use]
    pub fn with_indent(mut self, indent: usize) -> Self {
        self.indent = indent.clamp(1, 9);
        self
    }

    /// Set line width (clamped to 20-1000).
    #[must_use]
    pub fn with_width(mut self, width: usize) -> Self {
        self.width = width.clamp(20, 1000);
        self
    }

    /// Set default flow style for collections.
    #[must_use]
    pub const fn with_default_flow_style(mut self, flow_style: Option<bool>) -> Self {
        self.default_flow_style = flow_style;
        self
    }

    /// Set explicit document start marker.
    #[must_use]
    pub const fn with_explicit_start(mut self, explicit_start: bool) -> Self {
        self.explicit_start = explicit_start;
        self
    }

    /// Set compact inline notation.
    #[must_use]
    pub const fn with_compact(mut self, compact: bool) -> Self {
        self.compact = compact;
        self
    }

    /// Set multiline string rendering.
    #[must_use]
    pub const fn with_multiline_strings(mut self, multiline_strings: bool) -> Self {
        self.multiline_strings = multiline_strings;
        self
    }
}

/// Emitter for YAML documents.
///
/// Wraps saphyr's `YamlEmitter` to provide a consistent API.
#[derive(Debug)]
pub struct Emitter;

impl Emitter {
    /// Emit a single YAML document to a string with configuration.
    ///
    /// # Errors
    ///
    /// Returns `EmitError::Emit` if the value cannot be serialized.
    ///
    /// # Examples
    ///
    /// ```
    /// use fast_yaml_core::{Emitter, EmitterConfig, Value};
    ///
    /// let value = Value::String("test".to_string());
    /// let config = EmitterConfig::new().with_explicit_start(true);
    /// let yaml = Emitter::emit_str_with_config(&value, &config)?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn emit_str_with_config(value: &Value, config: &EmitterConfig) -> EmitResult<String> {
        // When flow style is requested, use the custom path that renders {k: v} / [a, b].
        if config.default_flow_style == Some(true) {
            let raw = Self::emit_flow(value)?;
            let mut output = Self::apply_formatting(raw, config);
            if !output.is_empty() && !output.ends_with('\n') {
                output.push('\n');
            }
            return Ok(output);
        }

        let estimated_size = Self::estimate_output_size(value);
        let mut output = String::with_capacity(estimated_size);
        {
            let mut emitter = YamlEmitter::new(&mut output);

            // Apply saphyr native configuration
            emitter.compact(config.compact);
            emitter.multiline_strings(config.multiline_strings);

            // Convert YamlOwned to Yaml for emission
            let yaml_borrowed: saphyr::Yaml = value.into();
            emitter
                .dump(&yaml_borrowed)
                .map_err(|e| EmitError::Emit(e.to_string()))?;
        }

        // Apply post-processing for configuration options
        output = Self::apply_formatting(output, config);

        // Ensure output always ends with a newline
        if !output.is_empty() && !output.ends_with('\n') {
            output.push('\n');
        }

        Ok(output)
    }

    /// Estimate output size based on input value structure.
    fn estimate_output_size(value: &Value) -> usize {
        Self::estimate_value_size(value)
    }

    fn estimate_value_size(value: &Value) -> usize {
        match value {
            Value::Value(scalar) => Self::estimate_scalar_size(scalar),
            Value::Sequence(seq) => {
                // "- " prefix (2) + newline (1) per item + recursive content
                seq.iter().map(|v| 3 + Self::estimate_value_size(v)).sum()
            }
            Value::Mapping(map) => {
                // "key: " (~10) + newline (1) + recursive content
                map.iter()
                    .map(|(k, v)| 11 + Self::estimate_value_size(k) + Self::estimate_value_size(v))
                    .sum()
            }
            Value::Representation(s, _, _) => s.len() + 2,
            Value::Tagged(_, inner) => 10 + Self::estimate_value_size(inner),
            Value::Alias(_) => 10,
            Value::BadValue => 4,
        }
    }

    fn estimate_scalar_size(scalar: &ScalarOwned) -> usize {
        match scalar {
            ScalarOwned::Null => 4,       // "null"
            ScalarOwned::Boolean(_) => 5, // "false"
            ScalarOwned::Integer(i) => {
                // Decimal digits + sign (max 20 for i64)
                if *i == 0 {
                    1
                } else {
                    // Use checked_ilog10 for precise digit count without float conversion
                    i.unsigned_abs()
                        .checked_ilog10()
                        .map_or(1, |d| d as usize + 1)
                        + 1
                }
            }
            ScalarOwned::FloatingPoint(_) => 20, // Conservative estimate
            ScalarOwned::String(s) => s.len() + 2, // Possible quotes
        }
    }

    /// Emit a single YAML document to a string with default configuration.
    ///
    /// # Errors
    ///
    /// Returns `EmitError::Emit` if the value cannot be serialized.
    ///
    /// # Examples
    ///
    /// ```
    /// use fast_yaml_core::{Emitter, Value};
    ///
    /// let value = Value::String("test".to_string());
    /// let yaml = Emitter::emit_str(&value)?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn emit_str(value: &Value) -> EmitResult<String> {
        Self::emit_str_with_config(value, &EmitterConfig::default())
    }

    /// Emit multiple YAML documents to a string with document separators and configuration.
    ///
    /// # Errors
    ///
    /// Returns `EmitError::Emit` if any value cannot be serialized.
    ///
    /// # Examples
    ///
    /// ```
    /// use fast_yaml_core::{Emitter, EmitterConfig, Value};
    ///
    /// let docs = vec![
    ///     Value::String("first".to_string()),
    ///     Value::String("second".to_string()),
    /// ];
    /// let config = EmitterConfig::new().with_explicit_start(true);
    /// let yaml = Emitter::emit_all_with_config(&docs, &config)?;
    /// assert!(yaml.contains("---"));
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn emit_all_with_config(values: &[Value], config: &EmitterConfig) -> EmitResult<String> {
        // Pre-calculate total estimated size for all documents
        let total_size: usize =
            values.iter().map(Self::estimate_output_size).sum::<usize>() + values.len() * 5; // Account for "---\n" separators

        let mut output = String::with_capacity(total_size);

        // Create single config variant for non-first documents (avoids cloning per document)
        let inner_config = EmitterConfig {
            explicit_start: false,
            ..*config
        };

        for (i, value) in values.iter().enumerate() {
            // Add document separator before each document (except first if explicit_start is false)
            if i > 0 || config.explicit_start {
                output.push_str("---\n");
            }

            // Always use inner_config (with explicit_start=false) since we handle
            // document separators explicitly above
            let doc = Self::emit_str_with_config(value, &inner_config)?;
            output.push_str(&doc);

            // Ensure document ends with newline for proper separation
            if !output.ends_with('\n') {
                output.push('\n');
            }
        }

        Ok(output)
    }

    /// Emit multiple YAML documents to a string with document separators.
    ///
    /// # Errors
    ///
    /// Returns `EmitError::Emit` if any value cannot be serialized.
    ///
    /// # Examples
    ///
    /// ```
    /// use fast_yaml_core::{Emitter, Value};
    ///
    /// let docs = vec![
    ///     Value::String("first".to_string()),
    ///     Value::String("second".to_string()),
    /// ];
    /// let yaml = Emitter::emit_all(&docs)?;
    /// assert!(yaml.contains("---"));
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn emit_all(values: &[Value]) -> EmitResult<String> {
        Self::emit_all_with_config(values, &EmitterConfig::default())
    }

    /// Apply formatting configuration to YAML output.
    ///
    /// Handles `explicit_start` and potentially other post-processing.
    fn apply_formatting(mut output: String, config: &EmitterConfig) -> String {
        // Handle explicit_start
        if config.explicit_start {
            if !output.starts_with("---") {
                output.insert_str(0, "---\n");
            }
        } else if output.starts_with("---\n") {
            output.drain(..4);
        } else if output.starts_with("---") {
            // Find where content starts after "---"
            let skip = 3 + output[3..].chars().take_while(|c| *c == '\n').count();
            output.drain(..skip);
        }

        // Fix special float values for YAML 1.2 Core Schema compliance
        // saphyr outputs "inf"/"-inf"/"NaN", but YAML 1.2 requires ".inf"/"-.inf"/".nan"
        output = Self::fix_special_floats(&output);

        // Re-indent when caller requests a width other than saphyr's fixed 2 spaces.
        if config.indent != 2 {
            output = Self::reindent(&output, config.indent);
        }

        output
    }

    /// Fix special float values for YAML 1.2 Core Schema compliance.
    ///
    /// Converts saphyr's output format to YAML 1.2 compliant format:
    /// - `inf` → `.inf`
    /// - `-inf` → `-.inf`
    /// - `NaN` → `.nan`
    fn fix_special_floats(output: &str) -> String {
        if !Self::might_contain_special_floats(output) {
            return output.to_string();
        }

        // Slow path: line-by-line transformation
        Self::fix_special_floats_slow(output)
    }

    /// Quick check if output might contain special float patterns.
    /// Uses SIMD-accelerated memchr for speed - no regex or allocation.
    #[inline]
    fn might_contain_special_floats(output: &str) -> bool {
        let bytes = output.as_bytes();

        // Use SIMD-accelerated memmem for fast substring search
        // These are the only special float indicators in saphyr output
        memmem::find(bytes, b"inf").is_some() || memmem::find(bytes, b"NaN").is_some()
    }

    /// Slow path for `fix_special_floats`: processes line-by-line.
    /// Pre-allocates output buffer to avoid reallocations.
    fn fix_special_floats_slow(output: &str) -> String {
        // Pre-allocate output (same size as input since patterns are similar length)
        let mut result = String::with_capacity(output.len());

        for (i, line) in output.lines().enumerate() {
            if i > 0 {
                result.push('\n');
            }

            // Check if line ends with special float value (with optional whitespace)
            let trimmed = line.trim_end();
            if let Some(prefix) = trimmed.strip_suffix("inf") {
                // Check if it's "-inf" or standalone "inf"
                if let Some(before_minus) = prefix.strip_suffix('-') {
                    // Already has minus, check if it's at value position
                    if Self::is_value_position(before_minus) {
                        result.push_str(before_minus);
                        result.push_str("-.inf");
                        continue;
                    }
                } else if Self::is_value_position(prefix) {
                    result.push_str(prefix);
                    result.push_str(".inf");
                    continue;
                }
            } else if let Some(prefix) = trimmed.strip_suffix("NaN")
                && Self::is_value_position(prefix)
            {
                result.push_str(prefix);
                result.push_str(".nan");
                continue;
            }
            result.push_str(line);
        }

        result
    }

    /// Check if the prefix indicates this is a value position (after `: ` or start of line).
    fn is_value_position(prefix: &str) -> bool {
        prefix.is_empty()
            || prefix.ends_with(": ")
            || prefix.ends_with("- ")
            || prefix.ends_with('\n')
    }

    /// Extract `%YAML` and `%TAG` directive lines that appear before the first `---`.
    ///
    /// Returns the directive block (with a trailing newline) or an empty string.
    fn extract_directives(input: &str) -> String {
        let mut directives = String::new();
        for line in input.lines() {
            let trimmed = line.trim_start();
            if trimmed.starts_with("---") || trimmed.starts_with("...") {
                break;
            }
            if trimmed.starts_with("%YAML") || trimmed.starts_with("%TAG") {
                directives.push_str(line);
                directives.push('\n');
            }
        }
        directives
    }

    /// Format a YAML string with configuration.
    ///
    /// Uses streaming formatter for large files when the `streaming` feature is enabled,
    /// falling back to DOM-based formatting for small files or complex cases.
    ///
    /// Block scalar styles (`|` literal and `>` folded) are preserved in the output.
    /// `%YAML` and `%TAG` directives are extracted and prepended to the formatted output.
    ///
    /// # Errors
    ///
    /// Returns `EmitError::Emit` if the YAML cannot be parsed or formatted.
    ///
    /// # Examples
    ///
    /// ```
    /// use fast_yaml_core::{Emitter, EmitterConfig};
    ///
    /// let yaml = "key: value\nlist:\n  - item1\n  - item2\n";
    /// let config = EmitterConfig::default();
    /// let formatted = Emitter::format_with_config(yaml, &config)?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn format_with_config(input: &str, config: &EmitterConfig) -> EmitResult<String> {
        // Extract %YAML / %TAG directives before formatting; the streaming
        // formatter (and DOM fallback) silently drops them.
        let directives = Self::extract_directives(input);

        // Always prefer the streaming formatter when available.
        //
        // The DOM-based path (saphyr's YamlEmitter) quotes YAML 1.1 boolean-like
        // keys (`on`, `off`, `yes`, `no`) even though they are plain strings in
        // YAML 1.2.2 Core Schema. The streaming formatter preserves the original
        // ScalarStyle from the parser, so it never introduces spurious quoting.
        #[cfg(all(feature = "streaming", feature = "arena"))]
        {
            let formatted = crate::streaming::format_streaming_arena(input, config)?;
            Ok(Self::prepend_directives(&directives, formatted))
        }

        #[cfg(all(feature = "streaming", not(feature = "arena")))]
        {
            let formatted = crate::streaming::format_streaming(input, config)?;
            Ok(Self::prepend_directives(&directives, formatted))
        }

        // Non-streaming fallback: parse with early_parse=false to preserve block scalar styles
        // (literal | and folded >) instead of converting them to double-quoted strings.
        #[cfg(not(feature = "streaming"))]
        {
            let docs = crate::Parser::parse_all_preserving_styles(input)
                .map_err(|e| EmitError::Emit(e.to_string()))?;
            if docs.is_empty() {
                return Ok(String::new());
            }
            let inner_config = EmitterConfig {
                explicit_start: false,
                ..*config
            };
            let mut output = String::new();
            for (i, doc) in docs.iter().enumerate() {
                if i > 0 || config.explicit_start {
                    if !output.is_empty() && !output.ends_with('\n') {
                        output.push('\n');
                    }
                    output.push_str("---\n");
                }
                let emitted = Self::emit_str_preserving_styles(doc, &inner_config, 0)?;
                output.push_str(&emitted);
            }
            if !output.is_empty() && !output.ends_with('\n') {
                output.push('\n');
            }
            Ok(Self::prepend_directives(&directives, output))
        }
    }

    /// Prepend directive lines to formatted output.
    ///
    /// If `directives` is non-empty, inserts them before the first `---` line
    /// or at the beginning of the output.
    fn prepend_directives(directives: &str, formatted: String) -> String {
        if directives.is_empty() {
            return formatted;
        }
        // Per YAML 1.2 spec §6.8.1, a directive end marker (`---`) MUST follow
        // any directives. If the formatter already emitted `---`, just prepend the
        // directives; otherwise inject the required marker between them.
        if formatted.starts_with("---") {
            format!("{directives}{formatted}")
        } else {
            format!("{directives}---\n{formatted}")
        }
    }

    /// Emit a YAML value to a string, preserving block scalar styles.
    ///
    /// Handles `Literal` (`|`) and `Folded` (`>`) styles directly.
    /// All other nodes are delegated to the saphyr emitter.
    ///
    /// # Errors
    ///
    /// Returns `EmitError::Emit` if the value cannot be serialized.
    fn emit_str_preserving_styles(
        value: &Value,
        config: &EmitterConfig,
        indent_level: usize,
    ) -> EmitResult<String> {
        // Fast path: no block scalars — use standard saphyr emitter
        if !Self::has_block_scalar(value) {
            return Self::emit_str_with_config(value, config);
        }
        let raw = Self::emit_value(value, config, indent_level)?;
        // Apply the same post-processing (special floats, explicit_start) as the standard path
        Ok(Self::apply_formatting(raw, config))
    }

    /// Check whether the value tree contains any Literal or Folded block scalars.
    fn has_block_scalar(value: &Value) -> bool {
        match value {
            Value::Representation(_, ScalarStyle::Literal | ScalarStyle::Folded, _) => true,
            Value::Sequence(seq) => seq.iter().any(Self::has_block_scalar),
            Value::Mapping(map) => map
                .iter()
                .any(|(k, v)| Self::has_block_scalar(k) || Self::has_block_scalar(v)),
            Value::Tagged(_, inner) => Self::has_block_scalar(inner),
            _ => false,
        }
    }

    /// Recursively emit a YAML value, handling block scalars manually.
    ///
    /// `indent_level` is the current nesting depth (in units of `config.indent` spaces).
    /// Returns YAML text without a leading `---\n` document marker.
    fn emit_value(
        value: &Value,
        config: &EmitterConfig,
        indent_level: usize,
    ) -> EmitResult<String> {
        match value {
            Value::Representation(content, ScalarStyle::Literal, _) => Ok(
                Self::format_block_scalar(content, '|', config.indent, indent_level),
            ),
            Value::Representation(content, ScalarStyle::Folded, _) => Ok(
                Self::format_block_scalar(content, '>', config.indent, indent_level),
            ),
            Value::Mapping(map) => {
                let indent = " ".repeat(indent_level * config.indent);
                let mut out = String::new();
                for (k, v) in map {
                    let key_str = Self::emit_scalar_inline(k)?;
                    match v {
                        Value::Representation(_, ScalarStyle::Literal | ScalarStyle::Folded, _) => {
                            // Block scalar directly as value: `key: |\n  line\n`
                            let val_str = Self::emit_value(v, config, indent_level + 1)?;
                            write!(out, "{indent}{key_str}: {val_str}")
                                .map_err(|e| EmitError::Emit(e.to_string()))?;
                        }
                        Value::Mapping(_) | Value::Sequence(_) => {
                            writeln!(out, "{indent}{key_str}:")
                                .map_err(|e| EmitError::Emit(e.to_string()))?;
                            let val_str = Self::emit_value(v, config, indent_level + 1)?;
                            out.push_str(&val_str);
                        }
                        _ => {
                            let val_str = Self::emit_value_inline(v)?;
                            writeln!(out, "{indent}{key_str}: {val_str}")
                                .map_err(|e| EmitError::Emit(e.to_string()))?;
                        }
                    }
                }
                Ok(out)
            }
            Value::Sequence(seq) => {
                let indent = " ".repeat(indent_level * config.indent);
                let mut out = String::new();
                for item in seq {
                    match item {
                        Value::Representation(_, ScalarStyle::Literal | ScalarStyle::Folded, _) => {
                            let item_str = Self::emit_value(item, config, indent_level + 1)?;
                            write!(out, "{indent}- {item_str}")
                                .map_err(|e| EmitError::Emit(e.to_string()))?;
                        }
                        Value::Mapping(_) | Value::Sequence(_) => {
                            writeln!(out, "{indent}-")
                                .map_err(|e| EmitError::Emit(e.to_string()))?;
                            let item_str = Self::emit_value(item, config, indent_level + 1)?;
                            out.push_str(&item_str);
                        }
                        _ => {
                            let item_str = Self::emit_value_inline(item)?;
                            writeln!(out, "{indent}- {item_str}")
                                .map_err(|e| EmitError::Emit(e.to_string()))?;
                        }
                    }
                }
                Ok(out)
            }
            // Scalars and everything else: delegate to saphyr (no block style involved)
            _ => Self::emit_value_inline(value).map(|s| format!("{s}\n")),
        }
    }

    /// Emit a scalar key as an inline string (no trailing newline).
    fn emit_scalar_inline(value: &Value) -> EmitResult<String> {
        match value {
            Value::Representation(s, ScalarStyle::SingleQuoted, _) => Ok(format!("'{s}'")),
            Value::Representation(s, ScalarStyle::DoubleQuoted, _) => Ok(format!("\"{s}\"")),
            Value::Representation(s, _, _) => Ok(s.clone()),
            Value::Value(scalar) => match scalar {
                ScalarOwned::Null => Ok("null".to_string()),
                ScalarOwned::Boolean(b) => Ok(if *b { "true" } else { "false" }.to_string()),
                ScalarOwned::Integer(i) => Ok(i.to_string()),
                ScalarOwned::FloatingPoint(f) => {
                    let s = f.to_string();
                    // Ensure the output is recognisable as a float (YAML Core Schema).
                    // Rust formats e.g. `1.0` as `"1"` and `1.23e10` as `"12300000000"`.
                    // Append `.0` when the string contains no decimal point or exponent and
                    // is not a special value (inf / NaN handled by fix_special_floats).
                    if s.contains('.')
                        || s.contains('e')
                        || s.contains('E')
                        || s.eq_ignore_ascii_case("inf")
                        || s.eq_ignore_ascii_case("-inf")
                        || s.eq_ignore_ascii_case("nan")
                    {
                        Ok(s)
                    } else {
                        Ok(format!("{s}.0"))
                    }
                }
                ScalarOwned::String(s) => {
                    if s.contains(':') || s.contains('#') || s.is_empty() {
                        Ok(format!("\"{s}\""))
                    } else {
                        Ok(s.clone())
                    }
                }
            },
            _ => Err(EmitError::UnsupportedType(
                "complex key not supported".to_string(),
            )),
        }
    }

    /// Emit any non-block-scalar value as an inline string (no trailing newline).
    fn emit_value_inline(value: &Value) -> EmitResult<String> {
        let mut out = String::new();
        {
            let mut emitter = YamlEmitter::new(&mut out);
            emitter.compact(true);
            let yaml: saphyr::Yaml = value.into();
            emitter
                .dump(&yaml)
                .map_err(|e| EmitError::Emit(e.to_string()))?;
        }
        // saphyr emits "---\nvalue\n" — strip markers
        let trimmed = out
            .strip_prefix("---\n")
            .unwrap_or(&out)
            .trim_end_matches('\n');
        Ok(trimmed.to_string())
    }

    /// Format a block scalar header + indented body lines.
    ///
    /// Returns text starting with the block indicator (`|` or `>`),
    /// followed by `\n` and indented content lines.
    fn format_block_scalar(
        content: &str,
        indicator: char,
        indent_width: usize,
        indent_level: usize,
    ) -> String {
        let child_indent = " ".repeat(indent_level * indent_width);

        // Chomping: keep (+) if trailing blank lines, strip (-) if no trailing newline,
        // clip (default) otherwise.
        let chomping = if content.ends_with("\n\n") {
            "+"
        } else if !content.ends_with('\n') {
            "-"
        } else {
            ""
        };

        let mut out = format!("{indicator}{chomping}\n");
        for line in content.lines() {
            if line.is_empty() {
                out.push('\n');
            } else {
                // String::writeln never fails
                let _ = writeln!(out, "{child_indent}{line}");
            }
        }
        out
    }

    /// Emit a value in YAML flow style: mappings as `{k: v}`, sequences as `[a, b]`.
    ///
    /// Scalar values are rendered inline.  Nested collections are also rendered
    /// in flow style recursively.
    ///
    /// Returns a string without a leading `---\n` marker and without a trailing newline.
    fn emit_flow(value: &Value) -> EmitResult<String> {
        match value {
            Value::Mapping(map) => {
                let mut out = String::from("{");
                for (i, (k, v)) in map.iter().enumerate() {
                    if i > 0 {
                        out.push_str(", ");
                    }
                    let key_str = Self::emit_scalar_inline(k)?;
                    let val_str = Self::emit_flow(v)?;
                    write!(out, "{key_str}: {val_str}")
                        .map_err(|e| EmitError::Emit(e.to_string()))?;
                }
                out.push('}');
                Ok(out)
            }
            Value::Sequence(seq) => {
                let mut out = String::from("[");
                for (i, item) in seq.iter().enumerate() {
                    if i > 0 {
                        out.push_str(", ");
                    }
                    out.push_str(&Self::emit_flow(item)?);
                }
                out.push(']');
                Ok(out)
            }
            // Scalars: use the inline scalar renderer (no trailing newline)
            _ => Self::emit_value_inline(value),
        }
    }

    /// Re-indent saphyr output from 2-space indentation to `target` spaces per level.
    ///
    /// Lines that form block scalar bodies (content under `|` / `>`) retain their
    /// relative spacing; only the base indent level is rescaled.
    ///
    /// `---` / `...` markers and directive lines (`%YAML`, `%TAG`) are left unchanged.
    fn reindent(output: &str, target: usize) -> String {
        let mut result = String::with_capacity(output.len());
        let mut in_block_scalar = false;
        let mut block_scalar_base_indent: usize = 0;

        for (i, line) in output.lines().enumerate() {
            if i > 0 {
                result.push('\n');
            }

            // Directives and document markers: never re-indent.
            let trimmed = line.trim_start();
            if trimmed.starts_with("---")
                || trimmed.starts_with("...")
                || trimmed.starts_with("%YAML")
                || trimmed.starts_with("%TAG")
            {
                in_block_scalar = false;
                result.push_str(line);
                continue;
            }

            let leading = line.len() - trimmed.len();
            let level = leading / 2; // saphyr always uses 2-space indent

            if in_block_scalar {
                // Inside a block scalar body: keep lines that are deeper than the
                // mapping/sequence key that introduced the scalar.
                if leading > block_scalar_base_indent {
                    // Rescale: base_level * target + (extra spaces beyond base)
                    let base_level = block_scalar_base_indent / 2;
                    let extra = leading - block_scalar_base_indent;
                    let new_leading = base_level * target + extra;
                    let spaces = " ".repeat(new_leading);
                    result.push_str(&spaces);
                    result.push_str(trimmed);
                    continue;
                }
                // Dedented back out of the block scalar
                in_block_scalar = false;
            }

            // Detect start of block scalar: line ends with `|` or `>` (with optional
            // chomping indicator and trailing whitespace).
            let value_part = trimmed.trim_end_matches(|c: char| c.is_whitespace());
            let last_nonws = value_part.trim_start_matches(|c: char| c != '|' && c != '>');
            if last_nonws.starts_with('|') || last_nonws.starts_with('>') {
                in_block_scalar = true;
                block_scalar_base_indent = leading;
            }

            let new_leading = level * target;
            let spaces = " ".repeat(new_leading);
            result.push_str(&spaces);
            result.push_str(trimmed);
        }

        // Preserve trailing newline if present
        if output.ends_with('\n') {
            result.push('\n');
        }

        result
    }

    /// Format a YAML string with default configuration.
    ///
    /// Uses streaming formatter for large files when the `streaming` feature is enabled.
    ///
    /// # Errors
    ///
    /// Returns `EmitError::Emit` if the YAML cannot be parsed or formatted.
    ///
    /// # Examples
    ///
    /// ```
    /// use fast_yaml_core::Emitter;
    ///
    /// let yaml = "key: value\nlist:\n  - item1\n  - item2\n";
    /// let formatted = Emitter::format(yaml)?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn format(input: &str) -> EmitResult<String> {
        Self::format_with_config(input, &EmitterConfig::default())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use ordered_float::OrderedFloat;
    use saphyr::ScalarOwned;

    #[test]
    fn test_emit_str_string() {
        let value = Value::Value(ScalarOwned::String("test".to_string()));
        let result = Emitter::emit_str(&value).unwrap();
        assert!(result.contains("test"));
    }

    #[test]
    fn test_emit_str_integer() {
        let value = Value::Value(ScalarOwned::Integer(42));
        let result = Emitter::emit_str(&value).unwrap();
        assert!(result.contains("42"));
    }

    #[test]
    fn test_emit_all_multiple() {
        let values = vec![
            Value::Value(ScalarOwned::String("first".to_string())),
            Value::Value(ScalarOwned::String("second".to_string())),
        ];
        let result = Emitter::emit_all(&values).unwrap();
        assert!(result.contains("first"));
        assert!(result.contains("second"));
        assert!(result.contains("---"));
    }

    #[test]
    fn test_emit_all_single() {
        let values = vec![Value::Value(ScalarOwned::String("only".to_string()))];
        let result = Emitter::emit_all(&values).unwrap();
        assert!(result.contains("only"));
        assert!(!result.starts_with("---"));
    }

    #[test]
    fn test_emitter_config_default() {
        let config = EmitterConfig::default();
        assert_eq!(config.indent, 2);
        assert_eq!(config.width, 80);
        assert_eq!(config.default_flow_style, None);
        assert!(!config.explicit_start);
        assert!(config.compact);
        assert!(!config.multiline_strings);
    }

    #[test]
    fn test_emitter_config_builder() {
        let config = EmitterConfig::new()
            .with_indent(4)
            .with_width(120)
            .with_explicit_start(true)
            .with_compact(false);

        assert_eq!(config.indent, 4);
        assert_eq!(config.width, 120);
        assert!(config.explicit_start);
        assert!(!config.compact);
    }

    #[test]
    fn test_emitter_config_clamp_indent() {
        let config = EmitterConfig::new().with_indent(100);
        assert_eq!(config.indent, 9);

        let config = EmitterConfig::new().with_indent(0);
        assert_eq!(config.indent, 1);
    }

    #[test]
    fn test_emitter_config_clamp_width() {
        let config = EmitterConfig::new().with_width(10);
        assert_eq!(config.width, 20);

        let config = EmitterConfig::new().with_width(2000);
        assert_eq!(config.width, 1000);
    }

    #[test]
    fn test_emit_with_explicit_start() {
        let value = Value::Value(ScalarOwned::String("test".to_string()));
        let config = EmitterConfig::new().with_explicit_start(true);
        let result = Emitter::emit_str_with_config(&value, &config).unwrap();
        assert!(result.starts_with("---"));
    }

    #[test]
    fn test_emit_without_explicit_start() {
        let value = Value::Value(ScalarOwned::String("test".to_string()));
        let config = EmitterConfig::new().with_explicit_start(false);
        let result = Emitter::emit_str_with_config(&value, &config).unwrap();
        assert!(!result.starts_with("---"));
    }

    #[test]
    fn test_emit_all_with_explicit_start() {
        let values = vec![
            Value::Value(ScalarOwned::String("first".to_string())),
            Value::Value(ScalarOwned::String("second".to_string())),
        ];
        let config = EmitterConfig::new().with_explicit_start(true);
        let result = Emitter::emit_all_with_config(&values, &config).unwrap();
        assert!(result.starts_with("---"));
        assert_eq!(result.matches("---").count(), 2);
    }

    #[test]
    fn test_emit_with_compact_false() {
        let value = Value::Sequence(vec![
            Value::Value(ScalarOwned::Integer(1)),
            Value::Value(ScalarOwned::Integer(2)),
        ]);
        let config = EmitterConfig::new().with_compact(false);
        let result = Emitter::emit_str_with_config(&value, &config).unwrap();
        // Should contain formatting (exact format depends on saphyr)
        assert!(result.contains('1') && result.contains('2'));
    }

    #[test]
    fn test_emit_with_multiline_strings() {
        let value = Value::Value(ScalarOwned::String("line1\nline2".to_string()));
        let config = EmitterConfig::new().with_multiline_strings(true);
        let result = Emitter::emit_str_with_config(&value, &config).unwrap();
        // Should use literal block scalar notation (|)
        assert!(result.contains("line1") && result.contains("line2"));
    }

    #[test]
    fn test_estimate_scalar_size_all_types() {
        // Test Null
        let null_size = Emitter::estimate_scalar_size(&ScalarOwned::Null);
        assert_eq!(null_size, 4); // "null"

        // Test Boolean
        let bool_size = Emitter::estimate_scalar_size(&ScalarOwned::Boolean(true));
        assert_eq!(bool_size, 5); // "false" (conservative estimate)

        // Test Integer - edge cases
        // Zero case (special handling)
        let zero_size = Emitter::estimate_scalar_size(&ScalarOwned::Integer(0));
        assert_eq!(zero_size, 1);

        // Single digit
        let single_digit = Emitter::estimate_scalar_size(&ScalarOwned::Integer(5));
        assert!(single_digit >= 1);

        // Multi-digit positive
        let multi_digit = Emitter::estimate_scalar_size(&ScalarOwned::Integer(12345));
        assert!(multi_digit >= 5);

        // Negative number
        let negative = Emitter::estimate_scalar_size(&ScalarOwned::Integer(-42));
        assert!(negative >= 2); // "-" + digits

        // Test Float
        let float_size =
            Emitter::estimate_scalar_size(&ScalarOwned::FloatingPoint(OrderedFloat(1.23456)));
        assert_eq!(float_size, 20); // Conservative estimate

        // Test String
        let string_size = Emitter::estimate_scalar_size(&ScalarOwned::String("hello".to_string()));
        assert_eq!(string_size, 7); // 5 chars + 2 for possible quotes
    }

    #[test]
    fn test_estimate_value_size_mapping() {
        use saphyr::MappingOwned;

        // Create a mapping with string keys and integer values
        let mut map = MappingOwned::new();
        map.insert(
            Value::Value(ScalarOwned::String("key1".to_string())),
            Value::Value(ScalarOwned::Integer(100)),
        );
        map.insert(
            Value::Value(ScalarOwned::String("key2".to_string())),
            Value::Value(ScalarOwned::Integer(200)),
        );

        let mapping = Value::Mapping(map);
        let size = Emitter::estimate_value_size(&mapping);

        // Should be > 0 and account for both key-value pairs
        // Each pair has ~11 base overhead + key size + value size
        assert!(
            size > 20,
            "Mapping estimate should be significant: got {size}"
        );

        // Test nested mapping
        let mut nested_map = MappingOwned::new();
        nested_map.insert(
            Value::Value(ScalarOwned::String("outer".to_string())),
            mapping,
        );

        let nested_size = Emitter::estimate_value_size(&Value::Mapping(nested_map));
        assert!(
            nested_size > size,
            "Nested mapping should have larger estimate"
        );
    }

    #[test]
    fn test_might_contain_special_floats_positive() {
        // Direct "inf" patterns
        assert!(Emitter::might_contain_special_floats("inf"));
        assert!(Emitter::might_contain_special_floats("key: inf"));
        assert!(Emitter::might_contain_special_floats("-inf"));
        assert!(Emitter::might_contain_special_floats("key: -inf"));
        assert!(Emitter::might_contain_special_floats("- inf\n- -inf"));

        // Direct "NaN" patterns
        assert!(Emitter::might_contain_special_floats("NaN"));
        assert!(Emitter::might_contain_special_floats("key: NaN"));
        assert!(Emitter::might_contain_special_floats(
            "values:\n  - NaN\n  - inf"
        ));

        // Mixed content
        assert!(Emitter::might_contain_special_floats(
            "---\npi: 3.14\nspecial: inf\n"
        ));
    }

    #[test]
    fn test_might_contain_special_floats_false_positives() {
        // Words containing "inf" substring that will trigger the fast-path check
        // (but won't be converted because they're not in value positions)
        assert!(
            Emitter::might_contain_special_floats("information"),
            "'information' contains 'inf' substring"
        );
        assert!(
            Emitter::might_contain_special_floats("infinity"),
            "'infinity' contains 'inf' substring"
        );
        assert!(
            Emitter::might_contain_special_floats("infinite"),
            "'infinite' contains 'inf' substring"
        );
        assert!(
            Emitter::might_contain_special_floats("reinforce"),
            "'reinforce' contains 'inf' substring"
        );

        // Strings that should NOT trigger the check (no "inf" or "NaN" substring)
        assert!(!Emitter::might_contain_special_floats("hello world"));
        assert!(!Emitter::might_contain_special_floats("key: value"));
        assert!(!Emitter::might_contain_special_floats("number: 42"));
        assert!(!Emitter::might_contain_special_floats("pi: 3.14159"));
        assert!(!Emitter::might_contain_special_floats("config")); // "config" does NOT contain "inf"
        assert!(!Emitter::might_contain_special_floats("nan")); // lowercase "nan" != "NaN"
        assert!(!Emitter::might_contain_special_floats("INF")); // uppercase "INF" != "inf"
    }

    #[test]
    fn test_fix_special_floats_inf() {
        // Test standalone inf conversion
        let result = Emitter::fix_special_floats("inf");
        assert_eq!(result, ".inf");

        // Test inf in a mapping value position
        let result = Emitter::fix_special_floats("key: inf");
        assert_eq!(result, "key: .inf");

        // Test -inf conversion
        let result = Emitter::fix_special_floats("-inf");
        assert_eq!(result, "-.inf");

        // Test -inf in a mapping value position
        let result = Emitter::fix_special_floats("key: -inf");
        assert_eq!(result, "key: -.inf");

        // Test inf in a sequence
        let result = Emitter::fix_special_floats("- inf");
        assert_eq!(result, "- .inf");

        // Test -inf in a sequence
        let result = Emitter::fix_special_floats("- -inf");
        assert_eq!(result, "- -.inf");

        // Test mixed document with multiple inf values
        let input = "positive: inf\nnegative: -inf\nlist:\n  - inf\n  - -inf";
        let result = Emitter::fix_special_floats(input);
        assert!(result.contains("positive: .inf"));
        assert!(result.contains("negative: -.inf"));
        assert!(result.contains("- .inf"));
        assert!(result.contains("- -.inf"));
    }

    #[test]
    fn test_fix_special_floats_nan() {
        // Test standalone NaN conversion
        let result = Emitter::fix_special_floats("NaN");
        assert_eq!(result, ".nan");

        // Test NaN in a mapping value position
        let result = Emitter::fix_special_floats("value: NaN");
        assert_eq!(result, "value: .nan");

        // Test NaN in a sequence
        let result = Emitter::fix_special_floats("- NaN");
        assert_eq!(result, "- .nan");

        // Test document with multiple NaN values
        let input = "nan_value: NaN\nlist:\n  - NaN";
        let result = Emitter::fix_special_floats(input);
        assert!(result.contains("nan_value: .nan"));
        assert!(result.contains("- .nan"));

        // Test that strings containing "NaN" as part of word are not converted
        // (this relies on is_value_position check)
        let result = Emitter::fix_special_floats("name: BaNaNa");
        assert_eq!(result, "name: BaNaNa", "BaNaNa should not be modified");

        // Test mixed special floats
        let input = "inf_val: inf\nnan_val: NaN\nneg_inf: -inf";
        let result = Emitter::fix_special_floats(input);
        assert!(result.contains("inf_val: .inf"));
        assert!(result.contains("nan_val: .nan"));
        assert!(result.contains("neg_inf: -.inf"));
    }

    #[test]
    fn test_estimate_value_size_sequence() {
        let seq = Value::Sequence(vec![
            Value::Value(ScalarOwned::Integer(1)),
            Value::Value(ScalarOwned::Integer(2)),
            Value::Value(ScalarOwned::String("hello".to_string())),
        ]);

        let size = Emitter::estimate_value_size(&seq);

        // Each item: 3 (prefix "- " + newline) + scalar size
        // Item 1: 3 + 2 (digit + overhead) = 5
        // Item 2: 3 + 2 = 5
        // Item 3: 3 + 7 (5 chars + 2 quotes) = 10
        assert!(
            size >= 10,
            "Sequence estimate should be significant: got {size}"
        );
    }

    #[test]
    fn test_estimate_value_size_all_variants() {
        use saphyr_parser::{ScalarStyle, Tag};

        // Test Representation variant
        let repr = Value::Representation("custom".to_string(), ScalarStyle::Plain, None);
        let repr_size = Emitter::estimate_value_size(&repr);
        assert_eq!(repr_size, 8); // 6 chars + 2

        // Test Tagged variant
        let tag = Tag {
            handle: "!".to_string(),
            suffix: "custom".to_string(),
        };
        let tagged = Value::Tagged(tag, Box::new(Value::Value(ScalarOwned::Integer(42))));
        let tagged_size = Emitter::estimate_value_size(&tagged);
        // 10 (tag overhead) + inner value size
        assert!(tagged_size >= 10, "Tagged value should have tag overhead");

        // Test Alias variant (usize anchor ID)
        let alias = Value::Alias(1);
        let alias_size = Emitter::estimate_value_size(&alias);
        assert_eq!(alias_size, 10);

        // Test BadValue variant
        let bad = Value::BadValue;
        let bad_size = Emitter::estimate_value_size(&bad);
        assert_eq!(bad_size, 4);
    }

    #[test]
    fn test_emit_all_empty_slice() {
        let empty: Vec<Value> = vec![];
        let config = EmitterConfig::default();

        let result = Emitter::emit_all_with_config(&empty, &config).unwrap();
        assert!(result.is_empty(), "Empty input should produce empty output");
    }

    #[test]
    fn test_emit_all_buffer_preallocation() {
        // Create multiple documents to test buffer pre-allocation
        let docs: Vec<Value> = (0..10)
            .map(|i| Value::Value(ScalarOwned::String(format!("document_{i}"))))
            .collect();

        let config = EmitterConfig::default();
        let result = Emitter::emit_all_with_config(&docs, &config).unwrap();

        // Verify all documents are present
        for i in 0..10 {
            assert!(
                result.contains(&format!("document_{i}")),
                "Should contain document_{i}"
            );
        }

        // Verify document separators (9 separators for 10 documents)
        assert_eq!(
            result.matches("---").count(),
            9,
            "Should have 9 document separators"
        );
    }

    #[test]
    fn test_estimate_scalar_size_large_integer() {
        // Test max i64
        let max_int = Emitter::estimate_scalar_size(&ScalarOwned::Integer(i64::MAX));
        // i64::MAX = 9223372036854775807 (19 digits + potential sign)
        assert!(max_int >= 19, "Max i64 should have at least 19 chars");

        // Test min i64
        let min_int = Emitter::estimate_scalar_size(&ScalarOwned::Integer(i64::MIN));
        // i64::MIN = -9223372036854775808 (19 digits + sign)
        assert!(min_int >= 19, "Min i64 should have at least 19 chars");

        // Test powers of 10
        let thousand = Emitter::estimate_scalar_size(&ScalarOwned::Integer(1000));
        assert!(thousand >= 4, "1000 should have at least 4 chars");

        let million = Emitter::estimate_scalar_size(&ScalarOwned::Integer(1_000_000));
        assert!(million >= 7, "1000000 should have at least 7 chars");
    }

    #[test]
    fn test_might_contain_special_floats_empty() {
        assert!(!Emitter::might_contain_special_floats(""));
    }

    #[test]
    fn test_fix_special_floats_no_changes() {
        // Test output that doesn't contain special floats (fast path)
        let input = "key: value\nlist:\n  - item1\n  - item2\nnumber: 42\n";
        let result = Emitter::fix_special_floats(input);
        assert_eq!(result, input, "No changes should be made for normal YAML");
    }

    // Regression tests for issue #64: YAML 1.1 boolean-like keys must not be quoted.
    // In YAML 1.2.2 Core Schema, `on`, `off`, `yes`, `no` are plain strings.
    #[cfg(feature = "streaming")]
    #[test]
    fn test_format_yaml11_bool_key_on() {
        let result = Emitter::format("on: push").unwrap();
        assert!(
            !result.contains("\"on\""),
            "key `on` must not be quoted, got: {result}"
        );
        assert!(result.contains("on:"), "key `on` must appear unquoted");
    }

    #[cfg(feature = "streaming")]
    #[test]
    fn test_format_yaml11_bool_key_off() {
        let result = Emitter::format("off: value").unwrap();
        assert!(
            !result.contains("\"off\""),
            "key `off` must not be quoted, got: {result}"
        );
        assert!(result.contains("off:"), "key `off` must appear unquoted");
    }

    #[cfg(feature = "streaming")]
    #[test]
    fn test_format_yaml11_bool_key_yes() {
        let result = Emitter::format("yes: value").unwrap();
        assert!(
            !result.contains("\"yes\""),
            "key `yes` must not be quoted, got: {result}"
        );
        assert!(result.contains("yes:"), "key `yes` must appear unquoted");
    }

    #[cfg(feature = "streaming")]
    #[test]
    fn test_format_yaml11_bool_key_no() {
        let result = Emitter::format("no: value").unwrap();
        assert!(
            !result.contains("\"no\""),
            "key `no` must not be quoted, got: {result}"
        );
        assert!(result.contains("no:"), "key `no` must appear unquoted");
    }

    #[cfg(feature = "streaming")]
    #[test]
    fn test_format_github_actions_workflow() {
        let yaml = "on:\n  push:\n    branches:\n      - main\n";
        let result = Emitter::format(yaml).unwrap();
        assert!(
            !result.contains("\"on\""),
            "GitHub Actions `on:` trigger must not be quoted, got: {result}"
        );
        assert!(result.contains("on:"), "on: key must appear unquoted");
        assert!(result.contains("push:"), "push: key must appear");
    }

    #[cfg(feature = "streaming")]
    #[test]
    fn test_format_yaml12_bools_unaffected() {
        // YAML 1.2.2 actual booleans must still be emitted as true/false
        let yaml = "enabled: true\ndisabled: false\n";
        let result = Emitter::format(yaml).unwrap();
        assert!(result.contains("true"), "true value must be preserved");
        assert!(result.contains("false"), "false value must be preserved");
    }

    #[test]
    #[cfg(feature = "streaming")]
    fn test_format_preserves_float_types() {
        // Regression tests for issue #66: fy format must not change float type to integer

        // 1.0 must remain 1.0
        let result = Emitter::format("version: 1.0").unwrap();
        assert!(
            result.contains("1.0"),
            "version: 1.0 must emit as float, got: {result}"
        );
        assert!(
            !result.contains(": 1\n"),
            "version: 1.0 must not become integer 1, got: {result}"
        );

        // Scientific notation must be preserved
        let result = Emitter::format("count: 1.23e10").unwrap();
        assert!(
            result.contains("1.23e10") || result.contains("1.23e+10"),
            "Scientific notation must be preserved, got: {result}"
        );
        assert!(
            !result.contains("12300000000"),
            "Scientific notation must not expand to integer, got: {result}"
        );

        // Regular float preserved
        let result = Emitter::format("pi: 3.14").unwrap();
        assert!(
            result.contains("3.14"),
            "3.14 must be preserved, got: {result}"
        );
    }

    // Regression tests for issue #62: block scalar styles must be preserved by fy format.
    #[test]
    fn test_format_preserves_literal_block_scalar() {
        let input = "literal: |\n  line one\n  line two\n";
        let config = EmitterConfig::default();
        let result = Emitter::format_with_config(input, &config).unwrap();
        assert!(
            result.contains("literal: |"),
            "literal block style should be preserved, got: {result}"
        );
        assert!(result.contains("line one"));
        assert!(result.contains("line two"));
    }

    #[test]
    fn test_format_preserves_folded_block_scalar() {
        let input = "folded: >\n  word1\n  word2\n";
        let config = EmitterConfig::default();
        let result = Emitter::format_with_config(input, &config).unwrap();
        assert!(
            result.contains("folded: >"),
            "folded block style should be preserved, got: {result}"
        );
    }

    #[test]
    fn test_format_nested_literal_block_scalar() {
        let input = "outer:\n  inner: |\n    line one\n    line two\n";
        let config = EmitterConfig::default();
        let result = Emitter::format_with_config(input, &config).unwrap();
        assert!(
            result.contains("inner: |"),
            "nested literal block should be preserved, got: {result}"
        );
        assert!(result.contains("line one"));
        assert!(result.contains("line two"));
    }

    #[test]
    fn test_format_mixed_block_and_plain_values() {
        let input = "plain: value\nliteral: |\n  block content\nnumber: 42\n";
        let config = EmitterConfig::default();
        let result = Emitter::format_with_config(input, &config).unwrap();
        assert!(result.contains("plain: value"));
        assert!(result.contains("literal: |"));
        assert!(result.contains("block content"));
        assert!(result.contains("number: 42") || result.contains("number: '42'"));
    }

    #[test]
    fn test_format_block_scalar_not_double_quoted() {
        // Regression: before fix, block scalars were emitted as double-quoted strings
        let input = "key: |\n  multiline\n  content\n";
        let config = EmitterConfig::default();
        let result = Emitter::format_with_config(input, &config).unwrap();
        assert!(
            !result.contains("\"multiline"),
            "block scalar should not be double-quoted, got: {result}"
        );
        assert!(
            result.contains("key: |"),
            "literal indicator must be present, got: {result}"
        );
    }

    // Regression tests for issue #65: multi-document YAML stream formatting
    #[test]
    fn test_format_multidoc_preserves_both_documents() {
        let input =
            "---\n- Mark McGwire\n- Sammy Sosa\n\n---\n- Chicago Cubs\n- St Louis Cardinals";
        let result = Emitter::format(input).unwrap();
        assert!(result.contains("Mark McGwire"), "First doc must be present");
        assert!(
            result.contains("Chicago Cubs"),
            "Second doc must be present"
        );
    }

    #[test]
    fn test_format_multidoc_separator_present() {
        let input = "---\nfoo: 1\n---\nbar: 2";
        let result = Emitter::format(input).unwrap();
        assert!(result.contains("---"), "Separator must appear in output");
        assert!(result.contains("foo"), "First doc key must be present");
        assert!(result.contains("bar"), "Second doc key must be present");
    }

    #[test]
    fn test_format_multidoc_issue65_fixture() {
        let input =
            "---\n- Mark McGwire\n- Sammy Sosa\n\n---\n- Chicago Cubs\n- St Louis Cardinals";
        let result = Emitter::format(input).unwrap();
        assert!(result.contains("Mark McGwire"));
        assert!(result.contains("Sammy Sosa"));
        assert!(result.contains("Chicago Cubs"));
        assert!(result.contains("St Louis Cardinals"));
        assert!(result.contains("---"));
    }

    #[test]
    fn test_format_multidoc_three_documents() {
        let input = "---\na: 1\n---\nb: 2\n---\nc: 3";
        let result = Emitter::format(input).unwrap();
        assert!(result.contains('a'), "First doc must be present");
        assert!(result.contains('b'), "Second doc must be present");
        assert!(result.contains('c'), "Third doc must be present");
        assert!(
            result.matches("---").count() >= 2,
            "At least two separators must appear between three documents"
        );
    }

    #[test]
    fn test_format_multidoc_explicit_start() {
        let input = "---\nfoo: 1\n---\nbar: 2";
        let config = EmitterConfig::new().with_explicit_start(true);
        let result = Emitter::format_with_config(input, &config).unwrap();
        assert!(result.contains("foo"), "First doc must be present");
        assert!(result.contains("bar"), "Second doc must be present");
        assert!(
            !result.contains("---\n---"),
            "Double separator must not appear: {result}"
        );
        assert_eq!(
            result.matches("---").count(),
            2,
            "Exactly two separators for two docs"
        );
    }

    // Regression tests for issue #75: formatter must not produce trailing spaces
    // or double-indented sequence-of-mapping keys.

    #[cfg(feature = "streaming")]
    #[test]
    fn test_format_nested_mapping_no_trailing_space() {
        // "parent:\n  child: value" — the colon after "parent" must not be followed
        // by a space when the value is a nested mapping.
        let result = Emitter::format("parent:\n  child: value\n").unwrap();
        assert!(
            !result.contains("parent: \n"),
            "trailing space after key with nested value, got: {result:?}"
        );
        assert!(
            result.contains("parent:\n"),
            "parent key must be followed by newline without space, got: {result:?}"
        );
        assert!(
            result.contains("child: value"),
            "child key-value must be preserved, got: {result:?}"
        );
    }

    #[cfg(feature = "streaming")]
    #[test]
    fn test_format_sequence_of_mappings_indent() {
        // Steps with mapping items — first key of each item must align with the key,
        // not be indented an extra level beyond "- ".
        let yaml = "steps:\n  - uses: actions/checkout@v4\n  - name: Install Rust\n    uses: dtolnay/rust-toolchain@stable\n";
        let result = Emitter::format(yaml).unwrap();
        assert!(
            !result.contains("-     "),
            "sequence item keys must not be double-indented, got: {result:?}"
        );
        assert!(
            result.contains("- uses:"),
            "first item key must directly follow dash, got: {result:?}"
        );
        assert!(
            result.contains("uses: actions/checkout@v4"),
            "got: {result:?}"
        );
        assert!(
            result.contains("uses: dtolnay/rust-toolchain@stable"),
            "got: {result:?}"
        );
    }

    #[cfg(feature = "streaming")]
    #[test]
    fn test_format_sequence_of_mappings_valid_yaml() {
        // Output must parse back to the same structure (no trailing spaces breaking YAML).
        let yaml = "steps:\n  - uses: actions/checkout@v4\n  - name: Install Rust\n    uses: dtolnay/rust-toolchain@stable\n";
        let result = Emitter::format(yaml).unwrap();
        let reparsed = crate::Parser::parse_str(&result);
        assert!(
            reparsed.is_ok(),
            "formatted output is invalid YAML: {result:?}"
        );
    }

    // Regression tests for issue #76: chomp indicator must not change during formatting.

    #[test]
    fn test_format_preserves_clip_chomp() {
        // `|` (clip) must remain `|`, not be converted to `|-`
        let input = "desc: |\n  line one\n  line two\n";
        let config = EmitterConfig::default();
        let result = Emitter::format_with_config(input, &config).unwrap();
        assert!(
            result.contains("desc: |\n"),
            "clip chomp `|` must not be changed to `|-`, got: {result}"
        );
    }

    #[test]
    fn test_format_preserves_strip_chomp() {
        // `|-` (strip) must remain `|-`
        let input = "desc: |-\n  line one\n  line two\n";
        let config = EmitterConfig::default();
        let result = Emitter::format_with_config(input, &config).unwrap();
        assert!(
            result.contains("desc: |-\n"),
            "strip chomp `|-` must be preserved, got: {result}"
        );
    }

    #[test]
    fn test_format_preserves_keep_chomp() {
        // `|+` (keep) must remain `|+` when value has trailing blank lines
        let input = "desc: |+\n  line one\n  line two\n\n";
        let config = EmitterConfig::default();
        let result = Emitter::format_with_config(input, &config).unwrap();
        assert!(
            result.contains("desc: |+\n"),
            "keep chomp `|+` must be preserved, got: {result}"
        );
    }

    #[test]
    fn test_format_preserves_folded_clip_chomp() {
        // `>` (folded clip) must remain `>`, not be converted to `>-`
        let input = "desc: >\n  line one\n  line two\n";
        let config = EmitterConfig::default();
        let result = Emitter::format_with_config(input, &config).unwrap();
        assert!(
            result.contains("desc: >\n"),
            "folded clip `>` must not be changed to `>-`, got: {result}"
        );
    }

    // Regression tests for issue #94: emit output must always end with newline
    #[test]
    fn test_emit_str_ends_with_newline() {
        let value = Value::Value(ScalarOwned::String("hello".to_string()));
        let result = Emitter::emit_str(&value).unwrap();
        assert!(
            result.ends_with('\n'),
            "emit_str output must end with newline, got: {result:?}"
        );
    }

    #[test]
    fn test_emit_str_with_config_ends_with_newline_default() {
        let value = Value::Value(ScalarOwned::String("hello".to_string()));
        let config = EmitterConfig::default();
        let result = Emitter::emit_str_with_config(&value, &config).unwrap();
        assert!(
            result.ends_with('\n'),
            "emit_str_with_config output must end with newline (default config), got: {result:?}"
        );
    }

    #[test]
    fn test_emit_str_with_config_ends_with_newline_explicit_start() {
        let value = Value::Value(ScalarOwned::String("hello".to_string()));
        let config = EmitterConfig::new().with_explicit_start(true);
        let result = Emitter::emit_str_with_config(&value, &config).unwrap();
        assert!(
            result.ends_with('\n'),
            "emit_str_with_config output must end with newline (explicit_start=true), got: {result:?}"
        );
    }

    // Regression tests for issue #95: format must preserve %YAML and %TAG directives
    #[test]
    fn test_format_preserves_yaml_directive() {
        let input = "%YAML 1.2\n---\nkey: value\n";
        let config = EmitterConfig::default();
        let result = Emitter::format_with_config(input, &config).unwrap();
        assert!(
            result.contains("%YAML 1.2"),
            "format_with_config must preserve %YAML directive, got: {result:?}"
        );
    }

    #[test]
    fn test_format_preserves_tag_directive() {
        let input = "%TAG ! tag:example.com,2000:app/\n---\nkey: value\n";
        let config = EmitterConfig::default();
        let result = Emitter::format_with_config(input, &config).unwrap();
        assert!(
            result.contains("%TAG ! tag:example.com,2000:app/"),
            "format_with_config must preserve %TAG directive, got: {result:?}"
        );
    }

    #[test]
    fn test_format_without_directives_works_normally() {
        let input = "key: value\n";
        let config = EmitterConfig::default();
        let result = Emitter::format_with_config(input, &config).unwrap();
        assert!(
            result.contains("key: value"),
            "format_with_config without directives must work normally, got: {result:?}"
        );
        assert!(
            result.ends_with('\n'),
            "format_with_config output must end with newline, got: {result:?}"
        );
    }

    #[test]
    fn test_format_yaml_directive_precedes_document_start() {
        let input = "%YAML 1.2\n---\nkey: value\n";
        let config = EmitterConfig::default();
        let result = Emitter::format_with_config(input, &config).unwrap();
        let yaml_pos = result
            .find("%YAML 1.2")
            .expect("%YAML directive must be present");
        let doc_start_pos = result.find("---").expect("--- must be present");
        assert!(
            yaml_pos < doc_start_pos,
            "%YAML directive must appear before ---, got: {result:?}"
        );
    }

    #[test]
    fn test_format_yaml_and_tag_directives_together() {
        let input = "%YAML 1.2\n%TAG ! tag:example.com,2000:app/\n---\nkey: value\n";
        let config = EmitterConfig::default();
        let result = Emitter::format_with_config(input, &config).unwrap();
        assert!(
            result.contains("%YAML 1.2"),
            "%YAML directive must be preserved, got: {result:?}"
        );
        assert!(
            result.contains("%TAG ! tag:example.com,2000:app/"),
            "%TAG directive must be preserved, got: {result:?}"
        );
        let yaml_pos = result.find("%YAML 1.2").unwrap();
        let tag_pos = result.find("%TAG").unwrap();
        let doc_pos = result.find("---").unwrap();
        assert!(
            yaml_pos < doc_pos,
            "%YAML must precede ---, got: {result:?}"
        );
        assert!(tag_pos < doc_pos, "%TAG must precede ---, got: {result:?}");
    }

    #[test]
    fn test_extract_directives_without_explicit_doc_start() {
        // extract_directives must capture %YAML lines even when no `---` follows in input.
        let input = "%YAML 1.2\nkey: value\n";
        let directives = Emitter::extract_directives(input);
        assert_eq!(
            directives, "%YAML 1.2\n",
            "extract_directives must return directive line even without ---, got: {directives:?}"
        );
    }

    #[test]
    fn test_format_directive_only_on_first_document_in_multidoc_stream() {
        let input = "%YAML 1.2\n---\nfirst: doc\n---\nsecond: doc\n";
        let config = EmitterConfig::default();
        let result = Emitter::format_with_config(input, &config).unwrap();
        assert!(
            result.contains("%YAML 1.2"),
            "%YAML directive must be present in output, got: {result:?}"
        );
        // Directive must appear only once (before first document)
        assert_eq!(
            result.matches("%YAML 1.2").count(),
            1,
            "Directive must appear exactly once, got: {result:?}"
        );
        assert!(
            result.contains("first: doc"),
            "first document must be present, got: {result:?}"
        );
        assert!(
            result.contains("second: doc"),
            "second document must be present, got: {result:?}"
        );
    }

    // Tests for issue #127: indent and default_flow_style must be applied.

    #[test]
    fn test_emit_with_indent_4() {
        use saphyr::MappingOwned;

        let mut map = MappingOwned::new();
        map.insert(
            Value::Value(ScalarOwned::String("key".to_string())),
            Value::Sequence(vec![
                Value::Value(ScalarOwned::Integer(1)),
                Value::Value(ScalarOwned::Integer(2)),
            ]),
        );
        let value = Value::Mapping(map);
        let config = EmitterConfig::new().with_indent(4);
        let result = Emitter::emit_str_with_config(&value, &config).unwrap();
        // With indent=4, list items under a key should start with 4 spaces.
        assert!(
            result.contains("    - 1") || result.contains("    -"),
            "indent=4 should produce 4-space indentation, got: {result:?}"
        );
    }

    #[test]
    fn test_emit_default_flow_style_true_mapping() {
        use saphyr::MappingOwned;

        let mut map = MappingOwned::new();
        map.insert(
            Value::Value(ScalarOwned::String("a".to_string())),
            Value::Value(ScalarOwned::Integer(1)),
        );
        map.insert(
            Value::Value(ScalarOwned::String("b".to_string())),
            Value::Value(ScalarOwned::Integer(2)),
        );
        let value = Value::Mapping(map);
        let config = EmitterConfig::new().with_default_flow_style(Some(true));
        let result = Emitter::emit_str_with_config(&value, &config).unwrap();
        assert!(
            result.contains('{') && result.contains('}'),
            "default_flow_style=true should produce flow mapping {{...}}, got: {result:?}"
        );
        assert!(result.contains("a: 1"), "mapping key a must be present");
        assert!(result.contains("b: 2"), "mapping key b must be present");
    }

    #[test]
    fn test_emit_default_flow_style_true_sequence() {
        let value = Value::Sequence(vec![
            Value::Value(ScalarOwned::Integer(1)),
            Value::Value(ScalarOwned::Integer(2)),
            Value::Value(ScalarOwned::Integer(3)),
        ]);
        let config = EmitterConfig::new().with_default_flow_style(Some(true));
        let result = Emitter::emit_str_with_config(&value, &config).unwrap();
        assert!(
            result.contains('[') && result.contains(']'),
            "default_flow_style=true should produce flow sequence [...], got: {result:?}"
        );
        assert!(result.contains("1, 2, 3"), "sequence items must be inline");
    }

    #[test]
    fn test_emit_default_flow_style_none_is_block() {
        let value = Value::Sequence(vec![
            Value::Value(ScalarOwned::Integer(1)),
            Value::Value(ScalarOwned::Integer(2)),
        ]);
        let config = EmitterConfig::new().with_default_flow_style(None);
        let result = Emitter::emit_str_with_config(&value, &config).unwrap();
        // Block style uses "- " prefix per item.
        assert!(
            result.contains("- 1"),
            "default_flow_style=None should produce block sequence, got: {result:?}"
        );
    }

    #[test]
    fn test_reindent_basic() {
        // saphyr emits 2-space indent; reindent to 4 should double it.
        let input = "key:\n  nested: value\n";
        let result = Emitter::reindent(input, 4);
        assert!(
            result.contains("    nested: value"),
            "reindent(4) should produce 4 spaces, got: {result:?}"
        );
    }

    #[test]
    fn test_reindent_preserves_markers() {
        let input = "---\nkey: value\n";
        let result = Emitter::reindent(input, 4);
        assert!(result.contains("---"), "--- marker must be preserved");
        assert!(result.contains("key: value"));
    }
}
