use std::collections::BTreeMap;
use std::fmt::Write;

use heck::ToUpperCamelCase;

use crate::schema::{DE_TABLE, MappingValue, MasterConfig, THEME_TABLE, ThemeMapping};

/// Escape a user-provided string for safe interpolation into a Rust string literal.
///
/// Handles backslash, double-quote, newline, carriage return, and tab.
pub(crate) fn escape_rust_str(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    for c in s.chars() {
        match c {
            '\\' => out.push_str("\\\\"),
            '"' => out.push_str("\\\""),
            '\n' => out.push_str("\\n"),
            '\r' => out.push_str("\\r"),
            '\t' => out.push_str("\\t"),
            other => out.push(other),
        }
    }
    out
}

/// Maps a known theme name string to its `IconSet::Variant` path (without crate prefix).
///
/// Returns `None` for unknown theme names (validation should catch these before codegen).
pub(crate) fn theme_name_to_icon_set(name: &str) -> Option<&'static str> {
    THEME_TABLE
        .iter()
        .find(|(k, _)| *k == name)
        .map(|(_, v)| *v)
}

/// Returns the fully-qualified icon set path with crate prefix.
pub(crate) fn theme_name_to_qualified_icon_set(name: &str, crate_path: &str) -> Option<String> {
    theme_name_to_icon_set(name).map(|variant| format!("{crate_path}::{variant}"))
}

/// Maps a lowercase TOML DE key to its `LinuxDesktop::Variant` path (without crate prefix).
///
/// Returns `None` for `"default"` (handled as the wildcard arm) and unknown keys.
pub(crate) fn de_key_to_variant(key: &str) -> Option<&'static str> {
    if key == "default" {
        return None;
    }
    DE_TABLE.iter().find(|(k, _)| *k == key).map(|(_, v)| *v)
}

/// Returns the fully-qualified DE variant path with crate prefix.
pub(crate) fn de_key_to_qualified_variant(key: &str, crate_path: &str) -> Option<String> {
    de_key_to_variant(key).map(|variant| format!("{crate_path}::{variant}"))
}

/// Generates a complete Rust source file implementing a custom icon enum with `IconProvider`.
///
/// The generated code includes:
/// - Header comment
/// - Optional `extern crate` declaration (when using default crate path)
/// - Enum definition with derives and `#[non_exhaustive]`
/// - `impl` block with `const ALL`
/// - `impl {crate_path}::IconProvider` with `icon_name()` and `icon_svg()` match arms
///
/// For DE-aware mapping values, `icon_name()` emits cfg-gated runtime dispatch
/// using `{crate_path}::detect_linux_de()`. On non-Linux, the default value is used.
///
/// When any DE-aware mapping exists, a `OnceLock`-based cache is emitted at the
/// start of `icon_name()` to avoid re-reading `XDG_CURRENT_DESKTOP` on every call.
pub(crate) fn generate_code(
    config: &MasterConfig,
    mappings: &BTreeMap<String, ThemeMapping>,
    base_dir: &str,
    crate_path: &str,
    extra_derives: &[String],
) -> String {
    let enum_name = config.name.to_upper_camel_case();
    let mut out = String::new();

    // Header comment
    let _ = writeln!(out, "// Generated by native-theme-build -- do not edit");
    let _ = writeln!(out);

    // When the default crate path is used, emit `extern crate` to support
    // Cargo aliases (e.g. `native-theme = {{ package = "native_theme" }}`).
    if crate_path == "native_theme" {
        let _ = writeln!(out, "extern crate native_theme;");
        let _ = writeln!(out);
    }

    // Enum definition
    let mut derives = String::from("Debug, Clone, Copy, PartialEq, Eq, Hash");
    for extra in extra_derives {
        let _ = write!(derives, ", {extra}");
    }
    let _ = writeln!(out, "#[derive({derives})]");
    let _ = writeln!(out, "#[non_exhaustive]");
    let _ = writeln!(out, "pub enum {enum_name} {{");
    for role in &config.roles {
        let variant = role.to_upper_camel_case();
        let _ = writeln!(out, "    {variant},");
    }
    let _ = writeln!(out, "}}");
    let _ = writeln!(out);

    // impl block with const ALL
    let _ = writeln!(out, "impl {enum_name} {{");
    let _ = writeln!(out, "    pub const ALL: &[Self] = &[");
    for role in &config.roles {
        let variant = role.to_upper_camel_case();
        let _ = writeln!(out, "        Self::{variant},");
    }
    let _ = writeln!(out, "    ];");
    let _ = writeln!(out, "}}");
    let _ = writeln!(out);

    // IconProvider impl
    let _ = writeln!(out, "impl {crate_path}::IconProvider for {enum_name} {{");

    // icon_name()
    generate_icon_name(&mut out, config, mappings, crate_path);

    let _ = writeln!(out);

    // icon_svg()
    generate_icon_svg(&mut out, config, mappings, base_dir, crate_path);

    let _ = writeln!(out, "}}");

    out
}

/// Returns `true` if any mapping value across all themes is DE-aware with
/// at least one non-default key that maps to a known DE variant.
fn has_any_de_aware_mappings(
    config: &MasterConfig,
    mappings: &BTreeMap<String, ThemeMapping>,
) -> bool {
    let all_themes = config
        .bundled_themes
        .iter()
        .chain(config.system_themes.iter());

    for theme_name in all_themes {
        if let Some(mapping) = mappings.get(theme_name) {
            for value in mapping.values() {
                if let MappingValue::DeAware(de_map) = value {
                    let has_de = de_map
                        .keys()
                        .any(|k| k.as_str() != "default" && de_key_to_variant(k).is_some());
                    if has_de {
                        return true;
                    }
                }
            }
        }
    }
    false
}

/// Generates the `icon_name` method body with match arms for all role-theme combinations.
fn generate_icon_name(
    out: &mut String,
    config: &MasterConfig,
    mappings: &BTreeMap<String, ThemeMapping>,
    crate_path: &str,
) {
    let _ = writeln!(
        out,
        "    fn icon_name(&self, set: {crate_path}::IconSet) -> Option<&str> {{"
    );

    // Issue 6: OnceLock DE caching -- if ANY mapping is DE-aware, emit a
    // cached DE detection at the top of the function.
    let needs_de_cache = has_any_de_aware_mappings(config, mappings);
    if needs_de_cache {
        let _ = writeln!(out, "        #[cfg(target_os = \"linux\")]");
        let _ = writeln!(out, "        let de = {{");
        let _ = writeln!(
            out,
            "            static CACHED_DE: std::sync::OnceLock<{crate_path}::LinuxDesktop> = std::sync::OnceLock::new();"
        );
        let _ = writeln!(
            out,
            "            *CACHED_DE.get_or_init(|| {crate_path}::detect_linux_de(&std::env::var(\"XDG_CURRENT_DESKTOP\").unwrap_or_default()))"
        );
        let _ = writeln!(out, "        }};");
    }

    let _ = writeln!(out, "        match (self, set) {{");

    // All themes (bundled + system) produce icon_name arms
    let all_themes = config
        .bundled_themes
        .iter()
        .chain(config.system_themes.iter());

    for theme_name in all_themes {
        // theme_name_to_qualified_icon_set returns None for unknown themes,
        // but validation has already ensured all themes are known by this point.
        let icon_set = match theme_name_to_qualified_icon_set(theme_name, crate_path) {
            Some(s) => s,
            None => continue,
        };
        if let Some(mapping) = mappings.get(theme_name) {
            for role in &config.roles {
                if let Some(mv) = mapping.get(role) {
                    let variant = role.to_upper_camel_case();
                    match mv {
                        MappingValue::Simple(name) => {
                            let escaped = escape_rust_str(name);
                            let _ = writeln!(
                                out,
                                "            (Self::{variant}, {icon_set}) => Some(\"{escaped}\"),"
                            );
                        }
                        MappingValue::DeAware(de_map) => {
                            // default_name() returns None only when "default" key is absent,
                            // but VAL-04 has already verified it exists.
                            let default_name = match de_map.get("default") {
                                Some(name) => name,
                                None => continue,
                            };
                            let escaped_default = escape_rust_str(default_name);
                            // Collect DE-specific entries (non-default keys)
                            let de_entries: Vec<_> = de_map
                                .iter()
                                .filter(|(k, _)| k.as_str() != "default")
                                .filter_map(|(k, v)| {
                                    de_key_to_qualified_variant(k, crate_path)
                                        .map(|variant_path| (variant_path, escape_rust_str(v)))
                                })
                                .collect();

                            if de_entries.is_empty() {
                                // No DE-specific overrides, collapse to simple arm
                                let _ = writeln!(
                                    out,
                                    "            (Self::{variant}, {icon_set}) => Some(\"{escaped_default}\"),"
                                );
                            } else {
                                // Emit cfg-gated DE-aware dispatch block using cached `de`
                                let _ = writeln!(
                                    out,
                                    "            (Self::{variant}, {icon_set}) => {{"
                                );
                                let _ =
                                    writeln!(out, "                #[cfg(target_os = \"linux\")]");
                                let _ = writeln!(out, "                {{");
                                let _ = writeln!(out, "                    match de {{");
                                for (variant_path, escaped_name) in &de_entries {
                                    let _ = writeln!(
                                        out,
                                        "                        {variant_path} => Some(\"{escaped_name}\"),"
                                    );
                                }
                                let _ = writeln!(
                                    out,
                                    "                        _ => Some(\"{escaped_default}\"),"
                                );
                                let _ = writeln!(out, "                    }}");
                                let _ = writeln!(out, "                }}");
                                let _ = writeln!(
                                    out,
                                    "                #[cfg(not(target_os = \"linux\"))]"
                                );
                                let _ = writeln!(out, "                {{");
                                let _ = writeln!(
                                    out,
                                    "                    Some(\"{escaped_default}\")"
                                );
                                let _ = writeln!(out, "                }}");
                                let _ = writeln!(out, "            }},");
                            }
                        }
                    }
                }
            }
        }
    }

    let _ = writeln!(out, "            _ => None,");
    let _ = writeln!(out, "        }}");
    let _ = writeln!(out, "    }}");
}

/// Generates the `icon_svg` method body with `include_bytes!` arms for bundled themes only.
fn generate_icon_svg(
    out: &mut String,
    config: &MasterConfig,
    mappings: &BTreeMap<String, ThemeMapping>,
    base_dir: &str,
    crate_path: &str,
) {
    let _ = writeln!(
        out,
        "    fn icon_svg(&self, set: {crate_path}::IconSet) -> Option<&'static [u8]> {{"
    );
    let _ = writeln!(out, "        match (self, set) {{");

    // Only bundled themes produce icon_svg arms
    for theme_name in &config.bundled_themes {
        let icon_set = match theme_name_to_qualified_icon_set(theme_name, crate_path) {
            Some(s) => s,
            None => continue,
        };
        if let Some(mapping) = mappings.get(theme_name) {
            for role in &config.roles {
                if let Some(mv) = mapping.get(role)
                    && let Some(icon_name) = mv.default_name()
                {
                    let variant = role.to_upper_camel_case();
                    let escaped_icon = escape_rust_str(icon_name);
                    let _ = writeln!(
                        out,
                        "            (Self::{variant}, {icon_set}) => Some(include_bytes!(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/{base_dir}/{theme_name}/{escaped_icon}.svg\"))),"
                    );
                }
            }
        }
    }

    let _ = writeln!(out, "            _ => None,");
    let _ = writeln!(out, "        }}");
    let _ = writeln!(out, "    }}");
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::BTreeMap;

    /// Build a test MasterConfig and mappings for a typical scenario:
    /// - enum name: "app-icon" (-> AppIcon)
    /// - roles: ["play-pause", "skip-forward"]
    /// - bundled themes: ["material"]
    /// - system themes: ["sf-symbols"]
    fn sample_config_and_mappings() -> (MasterConfig, BTreeMap<String, ThemeMapping>) {
        let config: MasterConfig = toml::from_str(
            r#"
name = "app-icon"
roles = ["play-pause", "skip-forward"]
bundled-themes = ["material"]
system-themes = ["sf-symbols"]
"#,
        )
        .unwrap();

        let material_mapping: ThemeMapping = toml::from_str(
            r#"
play-pause = "play_pause"
skip-forward = "skip_next"
"#,
        )
        .unwrap();

        let sf_mapping: ThemeMapping = toml::from_str(
            r#"
play-pause = "play.fill"
skip-forward = "forward.fill"
"#,
        )
        .unwrap();

        let mut mappings = BTreeMap::new();
        mappings.insert("material".to_string(), material_mapping);
        mappings.insert("sf-symbols".to_string(), sf_mapping);

        (config, mappings)
    }

    /// Build a config with a DE-aware mapping value.
    fn de_aware_config_and_mappings() -> (MasterConfig, BTreeMap<String, ThemeMapping>) {
        let config: MasterConfig = toml::from_str(
            r#"
name = "app-icon"
roles = ["play-pause"]
bundled-themes = ["material"]
system-themes = ["freedesktop"]
"#,
        )
        .unwrap();

        let material_mapping: ThemeMapping = toml::from_str(
            r#"
play-pause = "play_pause"
"#,
        )
        .unwrap();

        let freedesktop_mapping: ThemeMapping = toml::from_str(
            r#"
play-pause = { kde = "media-playback-start", default = "play" }
"#,
        )
        .unwrap();

        let mut mappings = BTreeMap::new();
        mappings.insert("material".to_string(), material_mapping);
        mappings.insert("freedesktop".to_string(), freedesktop_mapping);

        (config, mappings)
    }

    // === theme_name_to_icon_set tests ===

    #[test]
    fn theme_name_maps_sf_symbols() {
        assert_eq!(
            theme_name_to_icon_set("sf-symbols"),
            Some("IconSet::SfSymbols")
        );
    }

    #[test]
    fn theme_name_maps_segoe_fluent() {
        assert_eq!(
            theme_name_to_icon_set("segoe-fluent"),
            Some("IconSet::SegoeIcons")
        );
    }

    #[test]
    fn theme_name_maps_freedesktop() {
        assert_eq!(
            theme_name_to_icon_set("freedesktop"),
            Some("IconSet::Freedesktop")
        );
    }

    #[test]
    fn theme_name_maps_material() {
        assert_eq!(
            theme_name_to_icon_set("material"),
            Some("IconSet::Material")
        );
    }

    #[test]
    fn theme_name_maps_lucide() {
        assert_eq!(theme_name_to_icon_set("lucide"), Some("IconSet::Lucide"));
    }

    #[test]
    fn theme_name_returns_none_on_unknown() {
        assert_eq!(theme_name_to_icon_set("nonexistent"), None);
    }

    #[test]
    fn theme_name_qualified_includes_crate_path() {
        assert_eq!(
            theme_name_to_qualified_icon_set("material", "native_theme"),
            Some("native_theme::IconSet::Material".to_string())
        );
        assert_eq!(
            theme_name_to_qualified_icon_set("material", "my_crate"),
            Some("my_crate::IconSet::Material".to_string())
        );
    }

    // === generate_code: header and enum tests ===

    #[test]
    fn generated_code_has_header_comment() {
        let (config, mappings) = sample_config_and_mappings();
        let output = generate_code(&config, &mappings, "icons", "native_theme", &[]);
        assert!(output.starts_with("// Generated by native-theme-build -- do not edit"));
    }

    #[test]
    fn generated_code_has_derives() {
        let (config, mappings) = sample_config_and_mappings();
        let output = generate_code(&config, &mappings, "icons", "native_theme", &[]);
        assert!(output.contains("#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]"));
    }

    #[test]
    fn generated_code_has_non_exhaustive() {
        let (config, mappings) = sample_config_and_mappings();
        let output = generate_code(&config, &mappings, "icons", "native_theme", &[]);
        assert!(output.contains("#[non_exhaustive]"));
    }

    #[test]
    fn generated_code_has_enum_with_pascal_case_name() {
        let (config, mappings) = sample_config_and_mappings();
        let output = generate_code(&config, &mappings, "icons", "native_theme", &[]);
        assert!(output.contains("pub enum AppIcon {"));
    }

    #[test]
    fn generated_code_has_pascal_case_variants() {
        let (config, mappings) = sample_config_and_mappings();
        let output = generate_code(&config, &mappings, "icons", "native_theme", &[]);
        assert!(output.contains("PlayPause,"));
        assert!(output.contains("SkipForward,"));
    }

    #[test]
    fn generated_code_has_const_all() {
        let (config, mappings) = sample_config_and_mappings();
        let output = generate_code(&config, &mappings, "icons", "native_theme", &[]);
        assert!(output.contains("pub const ALL: &[Self]"));
        assert!(output.contains("Self::PlayPause,"));
        assert!(output.contains("Self::SkipForward,"));
    }

    // === generate_code: IconProvider impl tests ===

    #[test]
    fn generated_code_has_icon_provider_impl() {
        let (config, mappings) = sample_config_and_mappings();
        let output = generate_code(&config, &mappings, "icons", "native_theme", &[]);
        assert!(output.contains("impl native_theme::IconProvider for AppIcon"));
    }

    #[test]
    fn generated_code_has_icon_name_match_arms_for_bundled() {
        let (config, mappings) = sample_config_and_mappings();
        let output = generate_code(&config, &mappings, "icons", "native_theme", &[]);
        // material bundled theme should have icon_name arms
        assert!(
            output.contains(
                "(Self::PlayPause, native_theme::IconSet::Material) => Some(\"play_pause\")"
            ),
            "missing material play-pause icon_name arm. output:\n{output}"
        );
        assert!(
            output.contains(
                "(Self::SkipForward, native_theme::IconSet::Material) => Some(\"skip_next\")"
            ),
            "missing material skip-forward icon_name arm"
        );
    }

    #[test]
    fn generated_code_has_icon_name_match_arms_for_system() {
        let (config, mappings) = sample_config_and_mappings();
        let output = generate_code(&config, &mappings, "icons", "native_theme", &[]);
        // sf-symbols system theme should also have icon_name arms
        assert!(
            output.contains(
                "(Self::PlayPause, native_theme::IconSet::SfSymbols) => Some(\"play.fill\")"
            ),
            "missing sf-symbols play-pause icon_name arm. output:\n{output}"
        );
        assert!(
            output.contains(
                "(Self::SkipForward, native_theme::IconSet::SfSymbols) => Some(\"forward.fill\")"
            ),
            "missing sf-symbols skip-forward icon_name arm"
        );
    }

    #[test]
    fn generated_code_icon_name_has_wildcard() {
        let (config, mappings) = sample_config_and_mappings();
        let output = generate_code(&config, &mappings, "icons", "native_theme", &[]);
        // The icon_name match should end with _ => None
        assert!(
            output.contains("_ => None"),
            "missing wildcard arm in icon_name"
        );
    }

    // === generate_code: icon_svg tests ===

    #[test]
    fn generated_code_has_include_bytes_for_bundled() {
        let (config, mappings) = sample_config_and_mappings();
        let output = generate_code(&config, &mappings, "icons", "native_theme", &[]);
        assert!(
            output.contains(r#"include_bytes!(concat!(env!("CARGO_MANIFEST_DIR")"#),
            "missing include_bytes! with CARGO_MANIFEST_DIR"
        );
    }

    #[test]
    fn generated_code_include_bytes_uses_mapping_value_in_path() {
        let (config, mappings) = sample_config_and_mappings();
        let output = generate_code(&config, &mappings, "icons", "native_theme", &[]);
        // The SVG path should use the mapping value (play_pause), not the role name (play-pause)
        assert!(
            output.contains("/icons/material/play_pause.svg"),
            "SVG path should use mapping value 'play_pause', not role name. output:\n{output}"
        );
        assert!(
            output.contains("/icons/material/skip_next.svg"),
            "SVG path should use mapping value 'skip_next', not role name"
        );
    }

    #[test]
    fn generated_code_no_include_bytes_for_system_theme() {
        let (config, mappings) = sample_config_and_mappings();
        let output = generate_code(&config, &mappings, "icons", "native_theme", &[]);
        // System themes (sf-symbols) should NOT produce icon_svg match arms
        assert!(
            !output.contains("SfSymbols) => Some(include_bytes!"),
            "system themes should not produce include_bytes! arms"
        );
    }

    #[test]
    fn generated_code_icon_svg_has_wildcard() {
        let (config, mappings) = sample_config_and_mappings();
        let output = generate_code(&config, &mappings, "icons", "native_theme", &[]);
        // icon_svg function should have _ => None
        // We check this indirectly: the output must have at least two "_ => None" patterns
        // (one for icon_name, one for icon_svg)
        let count = output.matches("_ => None").count();
        assert!(
            count >= 2,
            "expected at least 2 wildcard arms (icon_name + icon_svg), found {count}"
        );
    }

    // === de_key_to_variant tests ===

    #[test]
    fn de_key_to_variant_maps_kde() {
        assert_eq!(de_key_to_variant("kde"), Some("LinuxDesktop::Kde"));
    }

    #[test]
    fn de_key_to_variant_maps_gnome() {
        assert_eq!(de_key_to_variant("gnome"), Some("LinuxDesktop::Gnome"));
    }

    #[test]
    fn de_key_to_variant_maps_xfce() {
        assert_eq!(de_key_to_variant("xfce"), Some("LinuxDesktop::Xfce"));
    }

    #[test]
    fn de_key_to_variant_maps_cinnamon() {
        assert_eq!(
            de_key_to_variant("cinnamon"),
            Some("LinuxDesktop::Cinnamon")
        );
    }

    #[test]
    fn de_key_to_variant_maps_mate() {
        assert_eq!(de_key_to_variant("mate"), Some("LinuxDesktop::Mate"));
    }

    #[test]
    fn de_key_to_variant_maps_lxqt() {
        assert_eq!(de_key_to_variant("lxqt"), Some("LinuxDesktop::LxQt"));
    }

    #[test]
    fn de_key_to_variant_maps_budgie() {
        assert_eq!(de_key_to_variant("budgie"), Some("LinuxDesktop::Budgie"));
    }

    #[test]
    fn de_key_to_variant_default_returns_none() {
        assert_eq!(de_key_to_variant("default"), None);
    }

    #[test]
    fn de_key_to_variant_unknown_returns_none() {
        assert_eq!(de_key_to_variant("cosmic"), None);
    }

    #[test]
    fn de_key_qualified_includes_crate_path() {
        assert_eq!(
            de_key_to_qualified_variant("kde", "native_theme"),
            Some("native_theme::LinuxDesktop::Kde".to_string())
        );
        assert_eq!(
            de_key_to_qualified_variant("kde", "my_crate"),
            Some("my_crate::LinuxDesktop::Kde".to_string())
        );
        assert_eq!(de_key_to_qualified_variant("default", "native_theme"), None);
    }

    // === generate_code: DE-aware value tests (updated from Phase 23) ===

    #[test]
    fn generated_code_de_aware_default_in_wildcard_arm() {
        let (config, mappings) = de_aware_config_and_mappings();
        let output = generate_code(&config, &mappings, "icons", "native_theme", &[]);
        // Phase 24: The default value "play" should appear inside a wildcard `_ =>` arm
        // within a `match de` block, NOT as a standalone simple arm
        assert!(
            output.contains("_ => Some(\"play\")"),
            "DE-aware default value 'play' should be in a wildcard arm. output:\n{output}"
        );
    }

    #[test]
    fn generated_code_de_aware_has_kde_specific_arm() {
        let (config, mappings) = de_aware_config_and_mappings();
        let output = generate_code(&config, &mappings, "icons", "native_theme", &[]);
        // Phase 24: KDE-specific arm "media-playback-start" SHOULD now appear
        assert!(
            output.contains("native_theme::LinuxDesktop::Kde => Some(\"media-playback-start\")"),
            "DE-aware codegen should generate KDE-specific arm. output:\n{output}"
        );
    }

    // === generate_code: new DE dispatch tests ===

    #[test]
    fn generated_code_de_aware_has_cfg_linux() {
        let (config, mappings) = de_aware_config_and_mappings();
        let output = generate_code(&config, &mappings, "icons", "native_theme", &[]);
        assert!(
            output.contains("#[cfg(target_os = \"linux\")]"),
            "DE-aware codegen should contain cfg linux gate. output:\n{output}"
        );
    }

    #[test]
    fn generated_code_de_aware_has_cfg_not_linux() {
        let (config, mappings) = de_aware_config_and_mappings();
        let output = generate_code(&config, &mappings, "icons", "native_theme", &[]);
        assert!(
            output.contains("#[cfg(not(target_os = \"linux\"))]"),
            "DE-aware codegen should contain cfg not-linux gate. output:\n{output}"
        );
    }

    #[test]
    fn generated_code_de_aware_calls_detect_linux_de() {
        let (config, mappings) = de_aware_config_and_mappings();
        let output = generate_code(&config, &mappings, "icons", "native_theme", &[]);
        assert!(
            output.contains("native_theme::detect_linux_de("),
            "DE-aware codegen should call detect_linux_de. output:\n{output}"
        );
    }

    #[test]
    fn generated_code_de_aware_not_linux_uses_default() {
        let (config, mappings) = de_aware_config_and_mappings();
        let output = generate_code(&config, &mappings, "icons", "native_theme", &[]);
        // The non-Linux cfg block should contain the default value
        // Check that there's a Some("play") within the non-linux block
        let cfg_not_linux_pos = output.find("#[cfg(not(target_os = \"linux\"))]");
        assert!(cfg_not_linux_pos.is_some(), "missing cfg not-linux block");
        let after_cfg = &output[cfg_not_linux_pos.unwrap()..];
        assert!(
            after_cfg.contains("Some(\"play\")"),
            "non-Linux block should use default value 'play'. output:\n{output}"
        );
    }

    /// Build a config with a DE-aware mapping that has ONLY a default key (no DE overrides).
    fn de_aware_default_only_config_and_mappings() -> (MasterConfig, BTreeMap<String, ThemeMapping>)
    {
        let config: MasterConfig = toml::from_str(
            r#"
name = "app-icon"
roles = ["play-pause"]
bundled-themes = ["material"]
system-themes = ["freedesktop"]
"#,
        )
        .unwrap();

        let material_mapping: ThemeMapping = toml::from_str(
            r#"
play-pause = "play_pause"
"#,
        )
        .unwrap();

        let freedesktop_mapping: ThemeMapping = toml::from_str(
            r#"
play-pause = { default = "play" }
"#,
        )
        .unwrap();

        let mut mappings = BTreeMap::new();
        mappings.insert("material".to_string(), material_mapping);
        mappings.insert("freedesktop".to_string(), freedesktop_mapping);

        (config, mappings)
    }

    #[test]
    fn generated_code_de_aware_default_only_produces_simple_arm() {
        let (config, mappings) = de_aware_default_only_config_and_mappings();
        let output = generate_code(&config, &mappings, "icons", "native_theme", &[]);
        // DeAware with only a default key (no DE overrides) should optimize to a simple arm
        assert!(
            output.contains(
                "(Self::PlayPause, native_theme::IconSet::Freedesktop) => Some(\"play\"),"
            ),
            "DE-aware with only default should produce simple arm. output:\n{output}"
        );
        // Should NOT contain cfg gating or detect_linux_de for this mapping
        assert!(
            !output.contains("detect_linux_de"),
            "DE-aware with only default should not use detect_linux_de. output:\n{output}"
        );
    }

    #[test]
    fn generated_code_simple_value_unchanged() {
        let (config, mappings) = de_aware_config_and_mappings();
        let output = generate_code(&config, &mappings, "icons", "native_theme", &[]);
        // The material theme has Simple("play_pause") -- should still produce a simple arm
        assert!(
            output.contains(
                "(Self::PlayPause, native_theme::IconSet::Material) => Some(\"play_pause\"),"
            ),
            "Simple mapping values should produce simple arms (no regression). output:\n{output}"
        );
    }

    // === escape_rust_str tests ===

    #[test]
    fn escape_rust_str_plain() {
        assert_eq!(escape_rust_str("play_pause"), "play_pause");
    }

    #[test]
    fn escape_rust_str_backslash() {
        assert_eq!(escape_rust_str(r"a\b"), r"a\\b");
    }

    #[test]
    fn escape_rust_str_double_quote() {
        assert_eq!(escape_rust_str(r#"say "hello""#), r#"say \"hello\""#);
    }

    #[test]
    fn escape_rust_str_newline() {
        assert_eq!(escape_rust_str("line1\nline2"), r"line1\nline2");
    }

    #[test]
    fn escape_rust_str_carriage_return() {
        assert_eq!(escape_rust_str("a\rb"), r"a\rb");
    }

    #[test]
    fn escape_rust_str_tab() {
        assert_eq!(escape_rust_str("a\tb"), r"a\tb");
    }

    #[test]
    fn escape_rust_str_combined() {
        assert_eq!(escape_rust_str("a\\b\n\"c\"\r\t"), r#"a\\b\n\"c\"\r\t"#);
    }

    #[test]
    fn generated_code_escapes_special_chars_in_mapping() {
        let config: MasterConfig = toml::from_str(
            r#"
name = "app-icon"
roles = ["test-role"]
bundled-themes = []
system-themes = ["sf-symbols"]
"#,
        )
        .unwrap();

        // Create a mapping with a value containing special characters
        let mut sf_mapping = BTreeMap::new();
        sf_mapping.insert(
            "test-role".to_string(),
            MappingValue::Simple("icon\"with\\special".to_string()),
        );

        let mut mappings = BTreeMap::new();
        mappings.insert("sf-symbols".to_string(), sf_mapping);

        let output = generate_code(&config, &mappings, "icons", "native_theme", &[]);
        assert!(
            output.contains(r#"Some("icon\"with\\special")"#),
            "special chars should be escaped. output:\n{output}"
        );
    }

    // === OnceLock DE caching tests (Issue 6) ===

    #[test]
    fn generated_code_de_aware_has_oncelock_cache() {
        let (config, mappings) = de_aware_config_and_mappings();
        let output = generate_code(&config, &mappings, "icons", "native_theme", &[]);
        assert!(
            output.contains("OnceLock"),
            "DE-aware codegen should emit OnceLock caching. output:\n{output}"
        );
        assert!(
            output.contains("CACHED_DE"),
            "DE-aware codegen should use CACHED_DE static. output:\n{output}"
        );
        assert!(
            output.contains("get_or_init"),
            "OnceLock should use get_or_init. output:\n{output}"
        );
    }

    #[test]
    fn generated_code_no_de_aware_no_oncelock() {
        let (config, mappings) = sample_config_and_mappings();
        let output = generate_code(&config, &mappings, "icons", "native_theme", &[]);
        assert!(
            !output.contains("OnceLock"),
            "Non-DE-aware codegen should not emit OnceLock. output:\n{output}"
        );
    }

    // === crate_path tests (Issue 14) ===

    #[test]
    fn generated_code_default_crate_path_has_extern_crate() {
        let (config, mappings) = sample_config_and_mappings();
        let output = generate_code(&config, &mappings, "icons", "native_theme", &[]);
        assert!(
            output.contains("extern crate native_theme;"),
            "default crate path should emit extern crate. output:\n{output}"
        );
    }

    #[test]
    fn generated_code_custom_crate_path_no_extern_crate() {
        let (config, mappings) = sample_config_and_mappings();
        let output = generate_code(&config, &mappings, "icons", "my_crate", &[]);
        assert!(
            !output.contains("extern crate"),
            "custom crate path should not emit extern crate. output:\n{output}"
        );
    }

    #[test]
    fn generated_code_custom_crate_path_uses_it() {
        let (config, mappings) = sample_config_and_mappings();
        let output = generate_code(&config, &mappings, "icons", "my_crate", &[]);
        assert!(
            output.contains("impl my_crate::IconProvider for AppIcon"),
            "should use custom crate path in impl. output:\n{output}"
        );
        assert!(
            output.contains("my_crate::IconSet"),
            "should use custom crate path for IconSet. output:\n{output}"
        );
        assert!(
            !output.contains("native_theme::"),
            "should not contain default crate path. output:\n{output}"
        );
    }

    #[test]
    fn generated_code_custom_crate_path_de_aware() {
        let (config, mappings) = de_aware_config_and_mappings();
        let output = generate_code(&config, &mappings, "icons", "my_crate", &[]);
        assert!(
            output.contains("my_crate::detect_linux_de("),
            "DE-aware codegen should use custom crate path. output:\n{output}"
        );
        assert!(
            output.contains("my_crate::LinuxDesktop"),
            "DE-aware codegen should use custom crate path for LinuxDesktop. output:\n{output}"
        );
    }

    // === extra_derives tests (Issue 16) ===

    #[test]
    fn generated_code_no_extra_derives() {
        let (config, mappings) = sample_config_and_mappings();
        let output = generate_code(&config, &mappings, "icons", "native_theme", &[]);
        assert!(
            output.contains("#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]"),
            "empty extra_derives should produce base derives only. output:\n{output}"
        );
    }

    #[test]
    fn generated_code_extra_derives_appended() {
        let (config, mappings) = sample_config_and_mappings();
        let extras = vec!["Ord".to_string(), "serde::Serialize".to_string()];
        let output = generate_code(&config, &mappings, "icons", "native_theme", &extras);
        assert!(
            output.contains(
                "#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, serde::Serialize)]"
            ),
            "extra derives should be appended. output:\n{output}"
        );
    }

    #[test]
    fn generated_code_single_extra_derive() {
        let (config, mappings) = sample_config_and_mappings();
        let extras = vec!["PartialOrd".to_string()];
        let output = generate_code(&config, &mappings, "icons", "native_theme", &extras);
        assert!(
            output.contains("#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd)]"),
            "single extra derive should be appended. output:\n{output}"
        );
    }
}
