//!
//! This module handles parsing and mapping markdownlint config files (JSON/YAML) to rumdl's internal config format.
//! It provides mapping from markdownlint rule keys to rumdl rule keys and provenance tracking for configuration values.

use crate::config::{ConfigSource, SourcedConfig, SourcedValue};
use serde::Deserialize;
use std::collections::HashMap;
use std::fs;

/// Represents a generic markdownlint config (rule keys to values)
#[derive(Debug, Deserialize)]
pub struct MarkdownlintConfig(pub HashMap<String, serde_yaml::Value>);

fn strip_jsonc_comments(content: &str) -> String {
    let mut result = String::with_capacity(content.len());
    let mut chars = content.chars().peekable();
    let mut in_string = false;
    let mut escape = false;
    let mut line_comment = false;
    let mut block_comment = false;

    while let Some(ch) = chars.next() {
        if line_comment {
            if ch == '\n' {
                line_comment = false;
                result.push('\n');
            }
            continue;
        }

        if block_comment {
            if ch == '*' && matches!(chars.peek(), Some('/')) {
                chars.next();
                block_comment = false;
            } else if ch == '\n' {
                result.push('\n');
            }
            continue;
        }

        if in_string {
            result.push(ch);
            if escape {
                escape = false;
            } else if ch == '\\' {
                escape = true;
            } else if ch == '"' {
                in_string = false;
            }
            continue;
        }

        if ch == '"' {
            in_string = true;
            result.push(ch);
            continue;
        }

        if ch == '/' {
            match chars.peek() {
                Some('/') => {
                    chars.next();
                    line_comment = true;
                    continue;
                }
                Some('*') => {
                    chars.next();
                    block_comment = true;
                    continue;
                }
                _ => {}
            }
        }

        result.push(ch);
    }

    result
}

/// Load a markdownlint config file (JSON or YAML) from the given path.
/// Supports both flat markdownlint format and markdownlint-cli2 format
/// where rules are nested under a top-level `config:` key.
pub fn load_markdownlint_config(path: &str) -> Result<MarkdownlintConfig, String> {
    let content = fs::read_to_string(path).map_err(|e| format!("Failed to read config file {path}: {e}"))?;

    let config: MarkdownlintConfig = if path.ends_with(".json") || path.ends_with(".jsonc") {
        let json_content = if path.ends_with(".jsonc") {
            strip_jsonc_comments(&content)
        } else {
            content.clone()
        };
        serde_json::from_str(&json_content).map_err(|e| format!("Failed to parse JSON: {e}"))?
    } else if path.ends_with(".yaml") || path.ends_with(".yml") {
        serde_yaml::from_str(&content).map_err(|e| format!("Failed to parse YAML: {e}"))?
    } else {
        let json_candidate = strip_jsonc_comments(&content);
        serde_json::from_str(&json_candidate)
            .or_else(|_| serde_yaml::from_str(&content))
            .map_err(|e| format!("Failed to parse config as JSON or YAML: {e}"))?
    };

    Ok(unwrap_cli2_config(config))
}

/// If the parsed config contains a top-level `config` key whose value is a mapping,
/// extract that mapping as the rule configuration. This supports the markdownlint-cli2
/// format where rules are nested under `config:`.
fn unwrap_cli2_config(config: MarkdownlintConfig) -> MarkdownlintConfig {
    if let Some(mapping) = config.0.get("config").and_then(|v| v.as_mapping()) {
        let inner_map: HashMap<String, serde_yaml::Value> = mapping
            .iter()
            .filter_map(|(k, v)| k.as_str().map(|s| (s.to_string(), v.clone())))
            .collect();
        return MarkdownlintConfig(inner_map);
    }
    config
}

/// Mapping table from markdownlint rule keys/aliases to rumdl rule keys
/// Convert a rule name (which may be an alias like "line-length") to the canonical rule ID (like "MD013").
/// Returns None if the rule name is not recognized.
pub fn markdownlint_to_rumdl_rule_key(key: &str) -> Option<&'static str> {
    // Use the shared alias resolution function from config module
    crate::config::resolve_rule_name_alias(key)
}

fn normalize_toml_table_keys(val: toml::Value) -> toml::Value {
    match val {
        toml::Value::Table(table) => {
            let mut new_table = toml::map::Map::new();
            for (k, v) in table {
                let norm_k = crate::config::normalize_key(&k);
                new_table.insert(norm_k, normalize_toml_table_keys(v));
            }
            toml::Value::Table(new_table)
        }
        toml::Value::Array(arr) => toml::Value::Array(arr.into_iter().map(normalize_toml_table_keys).collect()),
        other => other,
    }
}

/// Map markdownlint-specific option names to rumdl option names for a given rule.
/// This handles incompatibilities between markdownlint and rumdl config schemas.
/// Returns a new table with mapped options.
fn map_markdownlint_options_to_rumdl(
    rule_key: &str,
    table: toml::map::Map<String, toml::Value>,
) -> toml::map::Map<String, toml::Value> {
    let mut mapped = toml::map::Map::new();

    match rule_key {
        "MD013" => {
            // MD013 (line-length) has different option names in markdownlint vs rumdl
            for (k, v) in table {
                match k.as_str() {
                    // Markdownlint uses separate line length limits for different content types
                    // rumdl uses boolean flags to enable/disable checking for content types
                    "code-block-line-length" | "code_block_line_length" => {
                        // Ignore: rumdl doesn't support per-content-type line length limits
                        // Instead, users should use code-blocks = false to disable entirely
                        log::warn!(
                            "Ignoring markdownlint option 'code_block_line_length' for MD013. Use 'code-blocks = false' in rumdl to disable line length checking in code blocks."
                        );
                    }
                    "heading-line-length" | "heading_line_length" => {
                        // Ignore: rumdl doesn't support per-content-type line length limits
                        log::warn!(
                            "Ignoring markdownlint option 'heading_line_length' for MD013. Use 'headings = false' in rumdl to disable line length checking in headings."
                        );
                    }
                    "stern" => {
                        // Markdownlint uses "stern", rumdl uses "strict"
                        mapped.insert("strict".to_string(), v);
                    }
                    // Pass through all other options
                    _ => {
                        mapped.insert(k, v);
                    }
                }
            }
            mapped
        }
        "MD054" => {
            // MD054 (link-image-style) has fundamentally different config models
            // Markdownlint uses style/styles strings, rumdl uses individual boolean flags
            for (k, v) in table {
                match k.as_str() {
                    "style" | "styles" => {
                        // Ignore: rumdl uses individual boolean flags (autolink, inline, full, etc.)
                        // Cannot automatically map string style names to boolean flags
                        log::warn!(
                            "Ignoring markdownlint option '{k}' for MD054. rumdl uses individual boolean flags (autolink, inline, full, collapsed, shortcut, url-inline) instead. Please configure these directly."
                        );
                    }
                    // Pass through all other options (autolink, inline, full, collapsed, shortcut, url-inline)
                    _ => {
                        mapped.insert(k, v);
                    }
                }
            }
            mapped
        }
        // All other rules: pass through unchanged
        _ => table,
    }
}

/// Map a MarkdownlintConfig to rumdl's internal Config format
impl MarkdownlintConfig {
    /// Map to a SourcedConfig, tracking provenance as Markdownlint for all values.
    pub fn map_to_sourced_rumdl_config(&self, file_path: Option<&str>) -> SourcedConfig {
        let mut sourced_config = SourcedConfig::default();
        let file = file_path.map(std::string::ToString::to_string);

        // Extract the `default` key
        let default_enabled = self
            .0
            .get("default")
            .and_then(serde_yaml::Value::as_bool)
            .unwrap_or(true);

        let mut disabled_rules = Vec::new();
        let mut enabled_rules = Vec::new();

        for (key, value) in &self.0 {
            // Skip the `default` key — it's not a rule
            if key == "default" {
                continue;
            }

            let mapped = markdownlint_to_rumdl_rule_key(key);
            if let Some(rumdl_key) = mapped {
                let norm_rule_key = rumdl_key.to_ascii_uppercase();

                // Handle boolean values according to `default` semantics
                if value.is_bool() {
                    let is_enabled = value.as_bool().unwrap_or(false);
                    if default_enabled {
                        if !is_enabled {
                            disabled_rules.push(norm_rule_key.clone());
                        }
                    } else if is_enabled {
                        enabled_rules.push(norm_rule_key.clone());
                    }
                    continue;
                }

                let toml_value: Option<toml::Value> = serde_yaml::from_value::<toml::Value>(value.clone()).ok();
                let toml_value = toml_value.map(normalize_toml_table_keys);
                let rule_config = sourced_config.rules.entry(norm_rule_key.clone()).or_default();
                if let Some(tv) = toml_value {
                    if let toml::Value::Table(mut table) = tv {
                        // Apply markdownlint-to-rumdl option mapping
                        table = map_markdownlint_options_to_rumdl(&norm_rule_key, table);

                        // Special handling for MD007: Add style = "fixed" for markdownlint compatibility
                        if norm_rule_key == "MD007" && !table.contains_key("style") {
                            table.insert("style".to_string(), toml::Value::String("fixed".to_string()));
                        }

                        for (k, v) in table {
                            let norm_config_key = k; // Already normalized
                            rule_config
                                .values
                                .entry(norm_config_key.clone())
                                .and_modify(|sv| {
                                    sv.push_override(v.clone(), ConfigSource::ProjectConfig, file.clone());
                                })
                                .or_insert_with(|| SourcedValue {
                                    value: v,
                                    source: ConfigSource::ProjectConfig,
                                    origin: file.clone(),
                                });
                        }
                    } else {
                        rule_config
                            .values
                            .entry("value".to_string())
                            .and_modify(|sv| {
                                sv.push_override(tv.clone(), ConfigSource::ProjectConfig, file.clone());
                            })
                            .or_insert_with(|| SourcedValue {
                                value: tv,
                                source: ConfigSource::ProjectConfig,
                                origin: file.clone(),
                            });

                        // Special handling for MD007: Add style = "fixed" for markdownlint compatibility
                        if norm_rule_key == "MD007" && !rule_config.values.contains_key("style") {
                            rule_config.values.insert(
                                "style".to_string(),
                                SourcedValue {
                                    value: toml::Value::String("fixed".to_string()),
                                    source: ConfigSource::ProjectConfig,
                                    origin: file.clone(),
                                },
                            );
                        }
                    }
                    // When default: false, rules with object configs are explicitly enabled
                    if !default_enabled {
                        enabled_rules.push(norm_rule_key.clone());
                    }
                } else {
                    // The value could not be represented in rumdl's internal
                    // config format. Skip this rule and keep processing the rest
                    // rather than terminating the process.
                    log::error!(
                        "Could not convert value for rule key {key:?} to rumdl's internal config format. This likely means the configuration value is invalid or not supported for this rule. Please check your markdownlint config."
                    );
                }
            }
        }

        // Apply enable/disable lists
        if !disabled_rules.is_empty() {
            sourced_config.global.disable = SourcedValue::new(disabled_rules, ConfigSource::ProjectConfig);
        }
        if !enabled_rules.is_empty() || !default_enabled {
            sourced_config.global.enable = SourcedValue::new(enabled_rules, ConfigSource::ProjectConfig);
        }

        if let Some(f) = file {
            sourced_config.loaded_files.push(f);
        }
        sourced_config
    }

    /// Map to a SourcedConfigFragment, for use in config loading.
    pub fn map_to_sourced_rumdl_config_fragment(
        &self,
        file_path: Option<&str>,
    ) -> crate::config::SourcedConfigFragment {
        let mut fragment = crate::config::SourcedConfigFragment::default();
        let file = file_path.map(std::string::ToString::to_string);

        // Extract the `default` key: controls whether rules are enabled by default.
        // When true (or absent), all rules are enabled unless explicitly disabled.
        // When false, only rules explicitly set to true or configured with an object are enabled.
        let default_enabled = self
            .0
            .get("default")
            .and_then(serde_yaml::Value::as_bool)
            .unwrap_or(true);

        // Accumulate disabled and enabled rules
        let mut disabled_rules = Vec::new();
        let mut enabled_rules = Vec::new();

        for (key, value) in &self.0 {
            // Skip the `default` key — it's not a rule
            if key == "default" {
                continue;
            }

            let mapped = markdownlint_to_rumdl_rule_key(key);
            if let Some(rumdl_key) = mapped {
                let norm_rule_key = rumdl_key.to_ascii_uppercase();

                // Preserve the original key as the display name for import output.
                // If the user wrote "line-length", output [line-length] not [MD013].
                let display_name = if key.to_ascii_uppercase() == norm_rule_key {
                    norm_rule_key.clone()
                } else {
                    key.to_lowercase().replace('_', "-")
                };
                fragment
                    .rule_display_names
                    .insert(norm_rule_key.clone(), display_name.clone());

                // Special handling for boolean values (true/false)
                if value.is_bool() {
                    let enabled = value.as_bool().unwrap_or(false);
                    if default_enabled {
                        // default: true — all rules on by default
                        // true → no-op (already enabled), false → disable
                        if !enabled {
                            disabled_rules.push(display_name);
                        }
                    } else {
                        // default: false — all rules off by default
                        // true → enable, false → no-op (already disabled)
                        if enabled {
                            enabled_rules.push(display_name);
                        }
                    }
                    continue;
                }
                let toml_value: Option<toml::Value> = serde_yaml::from_value::<toml::Value>(value.clone()).ok();
                let toml_value = toml_value.map(normalize_toml_table_keys);
                if let Some(tv) = toml_value {
                    // Only materialize the rule entry once the value is known to
                    // be convertible, so an unconvertible value does not leave a
                    // phantom empty RuleConfig behind.
                    let rule_config = fragment.rules.entry(norm_rule_key.clone()).or_default();
                    // Special case: if line-length (MD013) is given a number value directly,
                    // treat it as {"line_length": value}
                    let tv = if norm_rule_key == "MD013" && tv.is_integer() {
                        let mut table = toml::map::Map::new();
                        table.insert("line-length".to_string(), tv);
                        toml::Value::Table(table)
                    } else {
                        tv
                    };

                    if let toml::Value::Table(mut table) = tv {
                        // Apply markdownlint-to-rumdl option mapping
                        table = map_markdownlint_options_to_rumdl(&norm_rule_key, table);

                        // Special handling for MD007: Add style = "fixed" for markdownlint compatibility
                        if norm_rule_key == "MD007" && !table.contains_key("style") {
                            table.insert("style".to_string(), toml::Value::String("fixed".to_string()));
                        }

                        for (rk, rv) in table {
                            let norm_rk = crate::config::normalize_key(&rk);
                            let sv = rule_config.values.entry(norm_rk.clone()).or_insert_with(|| {
                                crate::config::SourcedValue::new(rv.clone(), crate::config::ConfigSource::ProjectConfig)
                            });
                            sv.push_override(rv, crate::config::ConfigSource::ProjectConfig, file.clone());
                        }
                    } else {
                        rule_config
                            .values
                            .entry("value".to_string())
                            .and_modify(|sv| {
                                sv.push_override(tv.clone(), crate::config::ConfigSource::ProjectConfig, file.clone());
                            })
                            .or_insert_with(|| crate::config::SourcedValue {
                                value: tv,
                                source: crate::config::ConfigSource::ProjectConfig,
                                origin: file.clone(),
                            });

                        // Special handling for MD007: Add style = "fixed" for markdownlint compatibility
                        if norm_rule_key == "MD007" && !rule_config.values.contains_key("style") {
                            rule_config.values.insert(
                                "style".to_string(),
                                crate::config::SourcedValue {
                                    value: toml::Value::String("fixed".to_string()),
                                    source: crate::config::ConfigSource::ProjectConfig,
                                    origin: file.clone(),
                                },
                            );
                        }
                    }

                    // When default: false, rules with object configs are explicitly enabled
                    if !default_enabled {
                        enabled_rules.push(display_name.clone());
                    }
                } else {
                    log::error!(
                        "Could not convert value for rule key {key:?} to rumdl's internal config format; skipping this rule."
                    );
                }
            }
        }

        // Set all disabled rules at once
        if !disabled_rules.is_empty() {
            fragment.global.disable.push_override(
                disabled_rules,
                crate::config::ConfigSource::ProjectConfig,
                file.clone(),
            );
        }

        // Set all enabled rules at once.
        // When default: false, always push the enable override (even if empty)
        // so the source changes from Default to ProjectConfig, signaling that
        // the enable list is authoritative.
        if !enabled_rules.is_empty() || !default_enabled {
            fragment.global.enable.push_override(
                enabled_rules,
                crate::config::ConfigSource::ProjectConfig,
                file.clone(),
            );
        }

        if let Some(_f) = file {
            // SourcedConfigFragment does not have loaded_files, so skip
        }
        fragment
    }
}

// NOTE: 'code-block-style' (MD046) and 'code-fence-style' (MD048) are distinct and must not be merged. See markdownlint docs for details.

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;
    use tempfile::NamedTempFile;

    // ---- strip_jsonc_comments unit tests ----

    #[test]
    fn strip_jsonc_line_comment_removed() {
        let input = r#"{ "key": 1 } // trailing comment"#;
        assert_eq!(strip_jsonc_comments(input), r#"{ "key": 1 } "#);
    }

    #[test]
    fn strip_jsonc_block_comment_removed() {
        let input = r#"{ /* comment */ "key": 1 }"#;
        assert_eq!(strip_jsonc_comments(input), r#"{  "key": 1 }"#);
    }

    #[test]
    fn strip_jsonc_preserves_slash_slash_in_string() {
        // `//` inside a string literal must not be treated as a comment
        let input = r#"{ "url": "https://example.com" }"#;
        assert_eq!(strip_jsonc_comments(input), input);
    }

    #[test]
    fn strip_jsonc_preserves_block_comment_markers_in_string() {
        // `/*` and `*/` inside a string literal must not start/end a block comment
        let input = r#"{ "regex": "/* not a comment */" }"#;
        assert_eq!(strip_jsonc_comments(input), input);
    }

    #[test]
    fn strip_jsonc_slash_slash_inside_block_comment_is_ignored() {
        // `//` appearing inside a block comment must not end the block comment prematurely
        let input = "{ /* // still in block */ \"k\": 1 }";
        assert_eq!(strip_jsonc_comments(input), "{  \"k\": 1 }");
    }

    #[test]
    fn strip_jsonc_block_comment_newlines_preserved() {
        // Newlines inside block comments are kept so line numbers remain intact
        let input = "{\n/* line1\nline2 */\n\"k\": 1\n}";
        let result = strip_jsonc_comments(input);
        assert_eq!(result.lines().count(), input.lines().count());
    }

    #[test]
    fn strip_jsonc_unterminated_block_comment_drops_to_eof() {
        // Unterminated `/* ...` silently drops everything from the opener to EOF.
        // This produces invalid JSON, which the caller will detect and report.
        let input = r#"{ "k": 1 /* unclosed"#;
        let result = strip_jsonc_comments(input);
        assert!(
            !result.contains("unclosed"),
            "trailing content after /* should be dropped"
        );
        assert!(
            result.starts_with("{ \"k\": 1 "),
            "content before /* should be preserved"
        );
    }

    #[test]
    fn strip_jsonc_escaped_quote_in_string() {
        // Escaped `\"` inside a string must not end the string prematurely
        let input = r#"{ "msg": "say \"hi\" // still string" }"#;
        assert_eq!(strip_jsonc_comments(input), input);
    }

    // ---- markdownlint_to_rumdl_rule_key tests ----

    #[test]
    fn test_markdownlint_to_rumdl_rule_key() {
        // Test direct rule names
        assert_eq!(markdownlint_to_rumdl_rule_key("MD001"), Some("MD001"));
        assert_eq!(markdownlint_to_rumdl_rule_key("MD058"), Some("MD058"));

        // Test aliases with hyphens
        assert_eq!(markdownlint_to_rumdl_rule_key("heading-increment"), Some("MD001"));
        assert_eq!(markdownlint_to_rumdl_rule_key("HEADING-INCREMENT"), Some("MD001"));
        assert_eq!(markdownlint_to_rumdl_rule_key("ul-style"), Some("MD004"));
        assert_eq!(markdownlint_to_rumdl_rule_key("no-trailing-spaces"), Some("MD009"));
        assert_eq!(markdownlint_to_rumdl_rule_key("line-length"), Some("MD013"));
        assert_eq!(markdownlint_to_rumdl_rule_key("single-title"), Some("MD025"));
        assert_eq!(markdownlint_to_rumdl_rule_key("single-h1"), Some("MD025"));
        assert_eq!(markdownlint_to_rumdl_rule_key("no-bare-urls"), Some("MD034"));
        assert_eq!(markdownlint_to_rumdl_rule_key("code-block-style"), Some("MD046"));
        assert_eq!(markdownlint_to_rumdl_rule_key("code-fence-style"), Some("MD048"));

        // Test aliases with underscores (should also work)
        assert_eq!(markdownlint_to_rumdl_rule_key("heading_increment"), Some("MD001"));
        assert_eq!(markdownlint_to_rumdl_rule_key("HEADING_INCREMENT"), Some("MD001"));
        assert_eq!(markdownlint_to_rumdl_rule_key("ul_style"), Some("MD004"));
        assert_eq!(markdownlint_to_rumdl_rule_key("no_trailing_spaces"), Some("MD009"));
        assert_eq!(markdownlint_to_rumdl_rule_key("line_length"), Some("MD013"));
        assert_eq!(markdownlint_to_rumdl_rule_key("single_title"), Some("MD025"));
        assert_eq!(markdownlint_to_rumdl_rule_key("single_h1"), Some("MD025"));
        assert_eq!(markdownlint_to_rumdl_rule_key("no_bare_urls"), Some("MD034"));
        assert_eq!(markdownlint_to_rumdl_rule_key("code_block_style"), Some("MD046"));
        assert_eq!(markdownlint_to_rumdl_rule_key("code_fence_style"), Some("MD048"));

        // Test case insensitivity
        assert_eq!(markdownlint_to_rumdl_rule_key("md001"), Some("MD001"));
        assert_eq!(markdownlint_to_rumdl_rule_key("Md001"), Some("MD001"));
        assert_eq!(markdownlint_to_rumdl_rule_key("Line-Length"), Some("MD013"));
        assert_eq!(markdownlint_to_rumdl_rule_key("Line_Length"), Some("MD013"));

        // Test invalid keys
        assert_eq!(markdownlint_to_rumdl_rule_key("MD999"), None);
        assert_eq!(markdownlint_to_rumdl_rule_key("invalid-rule"), None);
        assert_eq!(markdownlint_to_rumdl_rule_key(""), None);
    }

    #[test]
    fn test_normalize_toml_table_keys() {
        use toml::map::Map;

        // Test table normalization
        let mut table = Map::new();
        table.insert("snake_case".to_string(), toml::Value::String("value1".to_string()));
        table.insert("kebab-case".to_string(), toml::Value::String("value2".to_string()));
        table.insert("MD013".to_string(), toml::Value::Integer(100));

        let normalized = normalize_toml_table_keys(toml::Value::Table(table));

        if let toml::Value::Table(norm_table) = normalized {
            assert!(norm_table.contains_key("snake-case"));
            assert!(norm_table.contains_key("kebab-case"));
            assert!(norm_table.contains_key("MD013"));
            assert_eq!(
                norm_table.get("snake-case").unwrap(),
                &toml::Value::String("value1".to_string())
            );
            assert_eq!(
                norm_table.get("kebab-case").unwrap(),
                &toml::Value::String("value2".to_string())
            );
        } else {
            panic!("Expected normalized value to be a table");
        }

        // Test array normalization
        let array = toml::Value::Array(vec![toml::Value::String("test".to_string()), toml::Value::Integer(42)]);
        let normalized_array = normalize_toml_table_keys(array.clone());
        assert_eq!(normalized_array, array);

        // Test simple value passthrough
        let simple = toml::Value::String("simple".to_string());
        assert_eq!(normalize_toml_table_keys(simple.clone()), simple);
    }

    #[test]
    fn test_load_markdownlint_config_json() {
        let mut temp_file = NamedTempFile::new().unwrap();
        writeln!(
            temp_file,
            r#"{{
            "MD013": {{ "line_length": 100 }},
            "MD025": true,
            "MD026": false,
            "heading-style": {{ "style": "atx" }}
        }}"#
        )
        .unwrap();

        let config = load_markdownlint_config(temp_file.path().to_str().unwrap()).unwrap();
        assert_eq!(config.0.len(), 4);
        assert!(config.0.contains_key("MD013"));
        assert!(config.0.contains_key("MD025"));
        assert!(config.0.contains_key("MD026"));
        assert!(config.0.contains_key("heading-style"));
    }

    #[test]
    fn test_load_markdownlint_config_yaml() {
        let mut temp_file = NamedTempFile::new().unwrap();
        writeln!(
            temp_file,
            r#"MD013:
  line_length: 120
MD025: true
MD026: false
ul-style:
  style: dash"#
        )
        .unwrap();

        let path = temp_file.path().with_extension("yaml");
        std::fs::rename(temp_file.path(), &path).unwrap();

        let config = load_markdownlint_config(path.to_str().unwrap()).unwrap();
        assert_eq!(config.0.len(), 4);
        assert!(config.0.contains_key("MD013"));
        assert!(config.0.contains_key("ul-style"));
    }

    #[test]
    fn test_load_markdownlint_config_invalid() {
        let mut temp_file = NamedTempFile::new().unwrap();
        writeln!(temp_file, "invalid json/yaml content {{").unwrap();

        let result = load_markdownlint_config(temp_file.path().to_str().unwrap());
        assert!(result.is_err());
    }

    #[test]
    fn test_load_markdownlint_config_nonexistent() {
        let result = load_markdownlint_config("/nonexistent/file.json");
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("Failed to read config file"));
    }

    #[test]
    fn test_map_to_sourced_rumdl_config() {
        let mut config_map = HashMap::new();
        config_map.insert(
            "MD013".to_string(),
            serde_yaml::Value::Mapping({
                let mut map = serde_yaml::Mapping::new();
                map.insert(
                    serde_yaml::Value::String("line_length".to_string()),
                    serde_yaml::Value::Number(serde_yaml::Number::from(100)),
                );
                map
            }),
        );
        config_map.insert("MD025".to_string(), serde_yaml::Value::Bool(true));
        config_map.insert("MD026".to_string(), serde_yaml::Value::Bool(false));

        let mdl_config = MarkdownlintConfig(config_map);
        let sourced_config = mdl_config.map_to_sourced_rumdl_config(Some("test.json"));

        // Check MD013 mapping
        assert!(sourced_config.rules.contains_key("MD013"));
        let md013_config = &sourced_config.rules["MD013"];
        assert!(md013_config.values.contains_key("line-length"));
        assert_eq!(md013_config.values["line-length"].value, toml::Value::Integer(100));
        assert_eq!(md013_config.values["line-length"].source, ConfigSource::ProjectConfig);

        // Check that loaded_files is tracked
        assert_eq!(sourced_config.loaded_files.len(), 1);
        assert_eq!(sourced_config.loaded_files[0], "test.json");
    }

    #[test]
    fn test_fragment_skips_unconvertible_value_cleanly() {
        let mut config_map = HashMap::new();
        // A YAML null has no toml::Value representation, so it hits the
        // conversion-failure branch on the production fragment path.
        config_map.insert("MD013".to_string(), serde_yaml::Value::Null);

        let mdl_config = MarkdownlintConfig(config_map);
        let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(None);

        // The unconvertible rule must be skipped entirely, not left behind as a
        // phantom empty RuleConfig entry.
        assert!(
            !fragment.rules.contains_key("MD013"),
            "unconvertible MD013 value should not create a rules entry, got {:?}",
            fragment.rules.get("MD013")
        );
    }

    #[test]
    fn test_legacy_map_skips_unconvertible_value_without_exiting() {
        let mut config_map = HashMap::new();
        config_map.insert("MD013".to_string(), serde_yaml::Value::Null);

        let mdl_config = MarkdownlintConfig(config_map);
        // Reaching the assertion proves the conversion-failure branch does not
        // call process::exit (which would kill the test runner).
        let sourced = mdl_config.map_to_sourced_rumdl_config(None);
        assert!(sourced.rules.get("MD013").is_none_or(|r| r.values.is_empty()));
    }

    #[test]
    fn test_map_to_sourced_rumdl_config_fragment() {
        let mut config_map = HashMap::new();

        // Test line-length alias for MD013 with numeric value
        config_map.insert(
            "line-length".to_string(),
            serde_yaml::Value::Number(serde_yaml::Number::from(120)),
        );

        // Test rule disable (false)
        config_map.insert("MD025".to_string(), serde_yaml::Value::Bool(false));

        // Test rule enable (true)
        config_map.insert("MD026".to_string(), serde_yaml::Value::Bool(true));

        // Test another rule with configuration
        config_map.insert(
            "MD003".to_string(),
            serde_yaml::Value::Mapping({
                let mut map = serde_yaml::Mapping::new();
                map.insert(
                    serde_yaml::Value::String("style".to_string()),
                    serde_yaml::Value::String("atx".to_string()),
                );
                map
            }),
        );

        let mdl_config = MarkdownlintConfig(config_map);
        let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));

        // Check that line-length (MD013) was properly configured
        assert!(fragment.rules.contains_key("MD013"));
        let md013_config = &fragment.rules["MD013"];
        assert!(md013_config.values.contains_key("line-length"));
        assert_eq!(md013_config.values["line-length"].value, toml::Value::Integer(120));

        // Check disabled rule
        assert!(fragment.global.disable.value.contains(&"MD025".to_string()));

        // When default is absent (= true), boolean true is no-op — no enable list
        assert!(
            !fragment.global.enable.value.contains(&"MD026".to_string()),
            "Boolean true should be no-op when default is absent (treated as true)"
        );
        assert!(fragment.global.enable.value.is_empty());

        // Check rule configuration
        assert!(fragment.rules.contains_key("MD003"));
        let md003_config = &fragment.rules["MD003"];
        assert!(md003_config.values.contains_key("style"));
    }

    #[test]
    fn test_edge_cases() {
        let mut config_map = HashMap::new();

        // Test empty config
        let empty_config = MarkdownlintConfig(HashMap::new());
        let sourced = empty_config.map_to_sourced_rumdl_config(None);
        assert!(sourced.rules.is_empty());

        // Test unknown rule (should be ignored)
        config_map.insert("unknown-rule".to_string(), serde_yaml::Value::Bool(true));
        config_map.insert("MD999".to_string(), serde_yaml::Value::Bool(true));

        let config = MarkdownlintConfig(config_map);
        let sourced = config.map_to_sourced_rumdl_config(None);
        assert!(sourced.rules.is_empty()); // Unknown rules should be ignored
    }

    #[test]
    fn test_complex_rule_configurations() {
        let mut config_map = HashMap::new();

        // Test MD044 with array configuration
        config_map.insert(
            "MD044".to_string(),
            serde_yaml::Value::Mapping({
                let mut map = serde_yaml::Mapping::new();
                map.insert(
                    serde_yaml::Value::String("names".to_string()),
                    serde_yaml::Value::Sequence(vec![
                        serde_yaml::Value::String("JavaScript".to_string()),
                        serde_yaml::Value::String("GitHub".to_string()),
                    ]),
                );
                map
            }),
        );

        // Test nested configuration
        config_map.insert(
            "MD003".to_string(),
            serde_yaml::Value::Mapping({
                let mut map = serde_yaml::Mapping::new();
                map.insert(
                    serde_yaml::Value::String("style".to_string()),
                    serde_yaml::Value::String("atx".to_string()),
                );
                map
            }),
        );

        let mdl_config = MarkdownlintConfig(config_map);
        let sourced = mdl_config.map_to_sourced_rumdl_config(None);

        // Verify MD044 configuration
        assert!(sourced.rules.contains_key("MD044"));
        let md044_config = &sourced.rules["MD044"];
        assert!(md044_config.values.contains_key("names"));

        // Verify MD003 configuration
        assert!(sourced.rules.contains_key("MD003"));
        let md003_config = &sourced.rules["MD003"];
        assert!(md003_config.values.contains_key("style"));
        assert_eq!(
            md003_config.values["style"].value,
            toml::Value::String("atx".to_string())
        );
    }

    #[test]
    fn test_value_types() {
        let mut config_map = HashMap::new();

        // Test different value types
        config_map.insert(
            "MD007".to_string(),
            serde_yaml::Value::Number(serde_yaml::Number::from(4)),
        ); // Simple number
        config_map.insert(
            "MD009".to_string(),
            serde_yaml::Value::Mapping({
                let mut map = serde_yaml::Mapping::new();
                map.insert(
                    serde_yaml::Value::String("br_spaces".to_string()),
                    serde_yaml::Value::Number(serde_yaml::Number::from(2)),
                );
                map.insert(
                    serde_yaml::Value::String("strict".to_string()),
                    serde_yaml::Value::Bool(true),
                );
                map
            }),
        );

        let mdl_config = MarkdownlintConfig(config_map);
        let sourced = mdl_config.map_to_sourced_rumdl_config(None);

        // Check simple number value
        assert!(sourced.rules.contains_key("MD007"));
        assert!(sourced.rules["MD007"].values.contains_key("value"));

        // Check complex configuration
        assert!(sourced.rules.contains_key("MD009"));
        let md009_config = &sourced.rules["MD009"];
        assert!(md009_config.values.contains_key("br-spaces"));
        assert!(md009_config.values.contains_key("strict"));
    }

    #[test]
    fn test_all_rule_aliases() {
        // Test that all documented aliases map correctly
        let aliases = vec![
            ("heading-increment", "MD001"),
            ("heading-style", "MD003"),
            ("ul-style", "MD004"),
            ("list-indent", "MD005"),
            ("ul-indent", "MD007"),
            ("no-trailing-spaces", "MD009"),
            ("no-hard-tabs", "MD010"),
            ("no-reversed-links", "MD011"),
            ("no-multiple-blanks", "MD012"),
            ("line-length", "MD013"),
            ("commands-show-output", "MD014"),
            // MD015-017 don't exist in markdownlint
            ("no-missing-space-atx", "MD018"),
            ("no-multiple-space-atx", "MD019"),
            ("no-missing-space-closed-atx", "MD020"),
            ("no-multiple-space-closed-atx", "MD021"),
            ("blanks-around-headings", "MD022"),
            ("heading-start-left", "MD023"),
            ("no-duplicate-heading", "MD024"),
            ("single-title", "MD025"),
            ("single-h1", "MD025"),
            ("no-trailing-punctuation", "MD026"),
            ("no-multiple-space-blockquote", "MD027"),
            ("no-blanks-blockquote", "MD028"),
            ("ol-prefix", "MD029"),
            ("list-marker-space", "MD030"),
            ("blanks-around-fences", "MD031"),
            ("blanks-around-lists", "MD032"),
            ("no-inline-html", "MD033"),
            ("no-bare-urls", "MD034"),
            ("hr-style", "MD035"),
            ("no-emphasis-as-heading", "MD036"),
            ("no-space-in-emphasis", "MD037"),
            ("no-space-in-code", "MD038"),
            ("no-space-in-links", "MD039"),
            ("fenced-code-language", "MD040"),
            ("first-line-heading", "MD041"),
            ("first-line-h1", "MD041"),
            ("no-empty-links", "MD042"),
            ("required-headings", "MD043"),
            ("proper-names", "MD044"),
            ("no-alt-text", "MD045"),
            ("code-block-style", "MD046"),
            ("single-trailing-newline", "MD047"),
            ("code-fence-style", "MD048"),
            ("emphasis-style", "MD049"),
            ("strong-style", "MD050"),
            ("link-fragments", "MD051"),
            ("reference-links-images", "MD052"),
            ("link-image-reference-definitions", "MD053"),
            ("link-image-style", "MD054"),
            ("table-pipe-style", "MD055"),
            ("table-column-count", "MD056"),
            ("existing-relative-links", "MD057"),
            ("blanks-around-tables", "MD058"),
            ("descriptive-link-text", "MD059"),
            ("table-cell-alignment", "MD060"),
            ("table-format", "MD060"),
            ("forbidden-terms", "MD061"),
            ("nested-code-fence", "MD070"),
            ("blank-line-after-frontmatter", "MD071"),
            ("frontmatter-key-sort", "MD072"),
        ];

        for (alias, expected) in aliases {
            assert_eq!(
                markdownlint_to_rumdl_rule_key(alias),
                Some(expected),
                "Alias {alias} should map to {expected}"
            );
        }
    }

    #[test]
    fn test_default_true_with_boolean_rules() {
        // default: true + MD001: true + MD013: { line_length: 120 }
        // Expected: no enable list (all rules already on), no disable list, MD013 config preserved
        let mut config_map = HashMap::new();
        config_map.insert("default".to_string(), serde_yaml::Value::Bool(true));
        config_map.insert("MD001".to_string(), serde_yaml::Value::Bool(true));
        config_map.insert(
            "MD013".to_string(),
            serde_yaml::Value::Mapping({
                let mut map = serde_yaml::Mapping::new();
                map.insert(
                    serde_yaml::Value::String("line_length".to_string()),
                    serde_yaml::Value::Number(serde_yaml::Number::from(120)),
                );
                map
            }),
        );

        let mdl_config = MarkdownlintConfig(config_map);
        let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));

        // No enable list: boolean true is no-op when default is true
        assert!(
            fragment.global.enable.value.is_empty(),
            "Enable list should be empty when default: true"
        );
        // No disable list
        assert!(fragment.global.disable.value.is_empty(), "Disable list should be empty");
        // MD013 config preserved
        assert!(fragment.rules.contains_key("MD013"));
        assert_eq!(
            fragment.rules["MD013"].values["line-length"].value,
            toml::Value::Integer(120)
        );
    }

    #[test]
    fn test_default_false_with_boolean_and_config_rules() {
        // default: false + MD001: true + MD013: { line_length: 120 }
        // Expected: enable list contains both MD001 and MD013
        let mut config_map = HashMap::new();
        config_map.insert("default".to_string(), serde_yaml::Value::Bool(false));
        config_map.insert("MD001".to_string(), serde_yaml::Value::Bool(true));
        config_map.insert(
            "MD013".to_string(),
            serde_yaml::Value::Mapping({
                let mut map = serde_yaml::Mapping::new();
                map.insert(
                    serde_yaml::Value::String("line_length".to_string()),
                    serde_yaml::Value::Number(serde_yaml::Number::from(120)),
                );
                map
            }),
        );

        let mdl_config = MarkdownlintConfig(config_map);
        let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));

        let mut enabled_sorted = fragment.global.enable.value.clone();
        enabled_sorted.sort();
        assert_eq!(
            enabled_sorted,
            vec!["MD001", "MD013"],
            "Both boolean-true and config-object rules should be in enable list"
        );
        assert!(fragment.global.disable.value.is_empty(), "No rules should be disabled");
        // MD013 config preserved
        assert!(fragment.rules.contains_key("MD013"));
        assert_eq!(
            fragment.rules["MD013"].values["line-length"].value,
            toml::Value::Integer(120)
        );
    }

    #[test]
    fn test_default_absent_with_boolean_rules() {
        // No `default` key + MD001: true → same as default: true (no enable list)
        let mut config_map = HashMap::new();
        config_map.insert("MD001".to_string(), serde_yaml::Value::Bool(true));
        config_map.insert("MD009".to_string(), serde_yaml::Value::Bool(false));

        let mdl_config = MarkdownlintConfig(config_map);
        let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));

        // No enable list: true is no-op when default is absent (treated as true)
        assert!(
            fragment.global.enable.value.is_empty(),
            "Enable list should be empty when default is absent"
        );
        // MD009 should be disabled
        assert_eq!(fragment.global.disable.value, vec!["MD009"]);
    }

    #[test]
    fn test_default_false_only_booleans() {
        // default: false + MD001: true + MD009: false
        // Expected: enable list = [MD001], no disable list (false is no-op when default: false)
        let mut config_map = HashMap::new();
        config_map.insert("default".to_string(), serde_yaml::Value::Bool(false));
        config_map.insert("MD001".to_string(), serde_yaml::Value::Bool(true));
        config_map.insert("MD009".to_string(), serde_yaml::Value::Bool(false));

        let mdl_config = MarkdownlintConfig(config_map);
        let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));

        assert_eq!(fragment.global.enable.value, vec!["MD001"]);
        assert!(
            fragment.global.disable.value.is_empty(),
            "Disable list should be empty when default: false (false is no-op)"
        );
    }

    #[test]
    fn test_default_true_with_boolean_rules_legacy() {
        // Test the legacy map_to_sourced_rumdl_config path
        let mut config_map = HashMap::new();
        config_map.insert("default".to_string(), serde_yaml::Value::Bool(true));
        config_map.insert("MD001".to_string(), serde_yaml::Value::Bool(true));
        config_map.insert("MD009".to_string(), serde_yaml::Value::Bool(false));
        config_map.insert(
            "MD013".to_string(),
            serde_yaml::Value::Mapping({
                let mut map = serde_yaml::Mapping::new();
                map.insert(
                    serde_yaml::Value::String("line_length".to_string()),
                    serde_yaml::Value::Number(serde_yaml::Number::from(120)),
                );
                map
            }),
        );

        let mdl_config = MarkdownlintConfig(config_map);
        let sourced = mdl_config.map_to_sourced_rumdl_config(Some("test.yaml"));

        // No enable list: boolean true is no-op when default is true
        assert!(sourced.global.enable.value.is_empty());
        // MD009 should be disabled
        assert_eq!(sourced.global.disable.value, vec!["MD009"]);
        // MD013 config preserved
        assert!(sourced.rules.contains_key("MD013"));
        assert_eq!(
            sourced.rules["MD013"].values["line-length"].value,
            toml::Value::Integer(120)
        );
    }

    #[test]
    fn test_default_false_with_config_rules_legacy() {
        // Test the legacy path with default: false
        let mut config_map = HashMap::new();
        config_map.insert("default".to_string(), serde_yaml::Value::Bool(false));
        config_map.insert("MD001".to_string(), serde_yaml::Value::Bool(true));
        config_map.insert(
            "MD013".to_string(),
            serde_yaml::Value::Mapping({
                let mut map = serde_yaml::Mapping::new();
                map.insert(
                    serde_yaml::Value::String("line_length".to_string()),
                    serde_yaml::Value::Number(serde_yaml::Number::from(120)),
                );
                map
            }),
        );

        let mdl_config = MarkdownlintConfig(config_map);
        let sourced = mdl_config.map_to_sourced_rumdl_config(Some("test.yaml"));

        let mut enabled_sorted = sourced.global.enable.value.clone();
        enabled_sorted.sort();
        assert_eq!(enabled_sorted, vec!["MD001", "MD013"]);
        assert!(sourced.global.disable.value.is_empty());
    }

    #[test]
    fn test_default_false_no_rules_disables_everything() {
        // default: false with no other rules should result in an empty-but-explicit enable list
        let mut config_map = HashMap::new();
        config_map.insert("default".to_string(), serde_yaml::Value::Bool(false));

        let mdl_config = MarkdownlintConfig(config_map);
        let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));

        // Enable list is empty but was explicitly set (source should be ProjectConfig, not Default)
        assert!(fragment.global.enable.value.is_empty());
        assert_eq!(
            fragment.global.enable.source,
            crate::config::ConfigSource::ProjectConfig,
            "Enable source should be ProjectConfig when default: false"
        );
    }

    #[test]
    fn test_default_false_only_false_rules_disables_everything() {
        // default: false + MD001: false → no rules enabled, enable list is explicit
        let mut config_map = HashMap::new();
        config_map.insert("default".to_string(), serde_yaml::Value::Bool(false));
        config_map.insert("MD001".to_string(), serde_yaml::Value::Bool(false));

        let mdl_config = MarkdownlintConfig(config_map);
        let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.yaml"));

        assert!(fragment.global.enable.value.is_empty());
        assert_eq!(
            fragment.global.enable.source,
            crate::config::ConfigSource::ProjectConfig,
        );
    }

    #[test]
    fn test_import_preserves_aliases_in_rules() {
        let mut config_map = HashMap::new();
        config_map.insert(
            "line-length".to_string(),
            serde_yaml::Value::Mapping({
                let mut map = serde_yaml::Mapping::new();
                map.insert(
                    serde_yaml::Value::String("line_length".to_string()),
                    serde_yaml::Value::Number(serde_yaml::Number::from(120)),
                );
                map
            }),
        );
        config_map.insert("no-bare-urls".to_string(), serde_yaml::Value::Bool(false));

        let mdl_config = MarkdownlintConfig(config_map);
        let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.json"));

        assert_eq!(fragment.rule_display_names.get("MD013").unwrap(), "line-length");
        assert_eq!(fragment.rule_display_names.get("MD034").unwrap(), "no-bare-urls");
    }

    #[test]
    fn test_import_preserves_canonical_ids() {
        let mut config_map = HashMap::new();
        config_map.insert(
            "MD013".to_string(),
            serde_yaml::Value::Mapping({
                let mut map = serde_yaml::Mapping::new();
                map.insert(
                    serde_yaml::Value::String("line_length".to_string()),
                    serde_yaml::Value::Number(serde_yaml::Number::from(120)),
                );
                map
            }),
        );
        config_map.insert("MD034".to_string(), serde_yaml::Value::Bool(false));

        let mdl_config = MarkdownlintConfig(config_map);
        let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.json"));

        assert_eq!(fragment.rule_display_names.get("MD013").unwrap(), "MD013");
        assert_eq!(fragment.rule_display_names.get("MD034").unwrap(), "MD034");
        assert!(fragment.global.disable.value.contains(&"MD034".to_string()));
    }

    #[test]
    fn test_import_mixed_aliases_and_ids() {
        let mut config_map = HashMap::new();
        config_map.insert(
            "line-length".to_string(),
            serde_yaml::Value::Mapping({
                let mut map = serde_yaml::Mapping::new();
                map.insert(
                    serde_yaml::Value::String("line_length".to_string()),
                    serde_yaml::Value::Number(serde_yaml::Number::from(120)),
                );
                map
            }),
        );
        config_map.insert("MD034".to_string(), serde_yaml::Value::Bool(false));

        let mdl_config = MarkdownlintConfig(config_map);
        let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.json"));

        // Alias is preserved
        assert_eq!(fragment.rule_display_names.get("MD013").unwrap(), "line-length");
        // Canonical ID is preserved
        assert_eq!(fragment.rule_display_names.get("MD034").unwrap(), "MD034");
    }

    #[test]
    fn test_import_disable_list_uses_aliases() {
        let mut config_map = HashMap::new();
        config_map.insert("line-length".to_string(), serde_yaml::Value::Bool(false));
        config_map.insert("no-bare-urls".to_string(), serde_yaml::Value::Bool(false));

        let mdl_config = MarkdownlintConfig(config_map);
        let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.json"));

        let mut disable_sorted = fragment.global.disable.value.clone();
        disable_sorted.sort();
        assert_eq!(disable_sorted, vec!["line-length", "no-bare-urls"]);
    }

    #[test]
    fn test_import_enable_list_uses_aliases_when_default_false() {
        let mut config_map = HashMap::new();
        config_map.insert("default".to_string(), serde_yaml::Value::Bool(false));
        config_map.insert("line-length".to_string(), serde_yaml::Value::Bool(true));
        config_map.insert("no-bare-urls".to_string(), serde_yaml::Value::Bool(true));

        let mdl_config = MarkdownlintConfig(config_map);
        let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.json"));

        let mut enable_sorted = fragment.global.enable.value.clone();
        enable_sorted.sort();
        assert_eq!(enable_sorted, vec!["line-length", "no-bare-urls"]);
    }

    #[test]
    fn test_import_underscore_aliases_normalized_to_kebab() {
        let mut config_map = HashMap::new();
        config_map.insert("no_bare_urls".to_string(), serde_yaml::Value::Bool(false));

        let mdl_config = MarkdownlintConfig(config_map);
        let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.json"));

        // Underscores in the original key are normalized to kebab-case
        assert_eq!(fragment.rule_display_names.get("MD034").unwrap(), "no-bare-urls");
        assert!(fragment.global.disable.value.contains(&"no-bare-urls".to_string()));
    }

    #[test]
    fn test_load_markdownlint_cli2_yaml_with_config_key() {
        let mut temp_file = NamedTempFile::new().unwrap();
        writeln!(
            temp_file,
            r#"config:
  MD013:
    line_length: 120
  MD025: true
  MD026: false
  ul-style:
    style: dash"#
        )
        .unwrap();

        let path = temp_file.path().with_extension("yaml");
        std::fs::rename(temp_file.path(), &path).unwrap();

        let config = load_markdownlint_config(path.to_str().unwrap()).unwrap();
        assert_eq!(config.0.len(), 4);
        assert!(config.0.contains_key("MD013"));
        assert!(config.0.contains_key("MD025"));
        assert!(config.0.contains_key("MD026"));
        assert!(config.0.contains_key("ul-style"));
    }

    #[test]
    fn test_load_markdownlint_cli2_json_with_config_key() {
        let mut temp_file = NamedTempFile::new().unwrap();
        writeln!(
            temp_file,
            r#"{{
            "config": {{
                "MD049": {{ "style": "asterisk" }},
                "MD013": {{ "line_length": 100 }}
            }}
        }}"#
        )
        .unwrap();

        let path = temp_file.path().with_extension("json");
        std::fs::rename(temp_file.path(), &path).unwrap();

        let config = load_markdownlint_config(path.to_str().unwrap()).unwrap();
        assert_eq!(config.0.len(), 2);
        assert!(config.0.contains_key("MD049"));
        assert!(config.0.contains_key("MD013"));
    }

    #[test]
    fn test_load_markdownlint_cli2_with_config_and_other_keys() {
        let mut temp_file = NamedTempFile::new().unwrap();
        writeln!(
            temp_file,
            r#"globs:
  - "**/*.md"
ignores:
  - "vendor/**"
config:
  MD013:
    line_length: 80
  MD049:
    style: underscore"#
        )
        .unwrap();

        let path = temp_file.path().with_extension("yaml");
        std::fs::rename(temp_file.path(), &path).unwrap();

        let config = load_markdownlint_config(path.to_str().unwrap()).unwrap();
        // Only rules from the config: key should be present, not globs/ignores
        assert_eq!(config.0.len(), 2);
        assert!(config.0.contains_key("MD013"));
        assert!(config.0.contains_key("MD049"));
        assert!(!config.0.contains_key("globs"));
        assert!(!config.0.contains_key("ignores"));
    }

    #[test]
    fn test_flat_format_still_works_with_config_as_rule() {
        // Flat format without a config: wrapper should continue to work
        let mut temp_file = NamedTempFile::new().unwrap();
        writeln!(
            temp_file,
            r#"MD013:
  line_length: 100
MD049:
  style: asterisk"#
        )
        .unwrap();

        let path = temp_file.path().with_extension("yaml");
        std::fs::rename(temp_file.path(), &path).unwrap();

        let config = load_markdownlint_config(path.to_str().unwrap()).unwrap();
        assert_eq!(config.0.len(), 2);
        assert!(config.0.contains_key("MD013"));
        assert!(config.0.contains_key("MD049"));
    }

    #[test]
    fn test_load_markdownlint_cli2_empty_config_mapping() {
        let mut temp_file = NamedTempFile::new().unwrap();
        writeln!(temp_file, "config: {{}}").unwrap();

        let path = temp_file.path().with_extension("yaml");
        std::fs::rename(temp_file.path(), &path).unwrap();

        let config = load_markdownlint_config(path.to_str().unwrap()).unwrap();
        assert!(
            config.0.is_empty(),
            "Empty config: mapping should produce empty rule set"
        );
    }

    #[test]
    fn test_scalar_config_key_not_treated_as_cli2_wrapper() {
        // A scalar `config: true` should NOT be treated as a cli2 wrapper
        let mut temp_file = NamedTempFile::new().unwrap();
        writeln!(
            temp_file,
            r#"config: true
MD013:
  line_length: 100"#
        )
        .unwrap();

        let path = temp_file.path().with_extension("yaml");
        std::fs::rename(temp_file.path(), &path).unwrap();

        let config = load_markdownlint_config(path.to_str().unwrap()).unwrap();
        // Both keys preserved — scalar "config" is not unwrapped
        assert_eq!(config.0.len(), 2);
        assert!(config.0.contains_key("config"));
        assert!(config.0.contains_key("MD013"));
    }

    #[test]
    fn test_import_case_insensitive_alias_preserved_lowercase() {
        let mut config_map = HashMap::new();
        config_map.insert("Line-Length".to_string(), serde_yaml::Value::Bool(false));

        let mdl_config = MarkdownlintConfig(config_map);
        let fragment = mdl_config.map_to_sourced_rumdl_config_fragment(Some("test.json"));

        // Display name is lowercased
        assert_eq!(fragment.rule_display_names.get("MD013").unwrap(), "line-length");
    }
}
