use std::rc::Rc;

// ── cell alignment ──────────────────────────────────────

/// Horizontal alignment override for a formatted cell.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum CellAlign {
    /// Left-aligned (default).
    #[default]
    Left,
    /// Centered.
    Center,
    /// Right-aligned.
    Right,
}

// ── formatted cell output ───────────────────────────────

/// Output of `format_cell()` — display text plus optional
/// style overrides.
#[derive(Debug, Clone, Default)]
pub struct FormattedCell {
    /// Display text after formatting.
    pub text: String,
    /// Alignment override (`None` = column default).
    pub align: Option<CellAlign>,
    /// Render in bold weight.
    pub bold: bool,
    /// Render in italic style.
    pub italic: bool,
    /// RGBA colour override. `None` = use theme default.
    pub color: Option<[u8; 4]>,
}

// ── styled cell element ─────────────────────────────────

/// A single visual element inside a styled cell.
///
/// `class` accepts space-separated DaisyUI / Tailwind class
/// names. The scene layer resolves them to canvas draw calls.
///
/// # Example
/// ```ignore
/// CellElement {
///     text:  "active".into(),
///     class: "badge badge-success".into(),
///     align: CellAlign::Left,
/// }
/// ```
#[derive(Debug, Clone)]
pub struct CellElement {
    /// Display text of the element.
    pub text: String,
    /// Space-separated DaisyUI/Tailwind classes.
    pub class: String,
    /// Horizontal alignment within the cell.
    pub align: CellAlign,
}

// ── cell format enum ────────────────────────────────────

/// Per-column display format.
///
/// Purely visual — does not affect the underlying data.
/// When `ColumnDef::format` is `Some`, the scene builder
/// pipes the raw string through `format_cell()` before
/// rendering.
#[non_exhaustive]
pub enum CellFormat {
    /// Numeric: 1234.5 → "1 234.50"
    Number {
        /// Number of digits after the decimal point.
        decimal_places: u8,
        /// e.g. `Some(' ')` or `Some(',')`.
        thousands_sep: Option<char>,
        /// e.g. `'.'` or `','`.
        decimal_sep: char,
    },
    /// Percentage: 0.75 → "75.00%"
    Percent {
        /// Number of digits after the decimal point.
        decimal_places: u8,
    },
    /// Currency: 42.5 → "$42.50"
    Currency {
        /// Currency symbol, e.g. `"$"` or `"€"`.
        symbol: String,
        /// Number of digits after the decimal point.
        decimal_places: u8,
        /// Thousands grouping character, e.g. `Some(',')`.
        thousands_sep: Option<char>,
        /// `true` → "42.50 €", `false` → "$42.50"
        symbol_after: bool,
    },
    /// Boolean: "true"/"1"/"yes" → true_label, else
    /// false_label.
    Boolean {
        /// Text shown for truthy values.
        true_label: String,
        /// Text shown for falsy values.
        false_label: String,
    },
    /// User-provided formatting callback.
    ///
    /// Uses [`Rc`] rather than [`Arc`] because the grid runs on a
    /// single thread (WASM / browser event loop). This makes
    /// `CellFormat` — and by extension `ColumnDef` and `GridState`
    /// — `!Send + !Sync`, which is intentional.
    ///
    /// [`Clone`] is implemented manually via `Rc::clone` (cheap
    /// reference-count bump, no deep copy of the closure).
    Custom(Rc<dyn Fn(&str) -> FormattedCell>),
    /// Image: cell value is a URL rendered as an image.
    Image {
        /// Optional base URL prefix. Final URL = base_url + raw.
        /// If `None`, raw value is the full URL.
        base_url: Option<String>,
        /// Corner radius in logical pixels (0 = sharp).
        border_radius: f64,
        /// Padding inside the cell around the image.
        padding: f64,
    },
    /// Styled multi-element layout using DaisyUI/Tailwind
    /// class names.
    ///
    /// The callback receives the raw cell value and returns
    /// a list of [`CellElement`]s. Each element carries a
    /// `class` string that the scene layer resolves to
    /// canvas drawing calls (rounded rect + text).
    ///
    /// Unknown class names are silently ignored.
    ///
    /// # Example
    /// ```ignore
    /// CellFormat::Styled(Rc::new(|raw| {
    ///     let class = match raw {
    ///         "true"  => "badge badge-success",
    ///         "false" => "badge badge-error badge-outline",
    ///         _       => "badge badge-neutral",
    ///     };
    ///     vec![CellElement {
    ///         text:  raw.to_string(),
    ///         class: class.to_string(),
    ///         align: CellAlign::Left,
    ///     }]
    /// }))
    /// ```
    #[allow(clippy::type_complexity)]
    Styled(Rc<dyn Fn(&str) -> Vec<CellElement>>),
    /// Image + text side by side (like AG Grid's flag
    /// cell renderer).
    ///
    /// Raw value = `"KEY Label"` — first token is the
    /// image key (lowercased for the URL), rest is the
    /// display text.
    ///
    /// Image URL = `{base_url}{key.lowercase()}{suffix}`.
    ImageText {
        /// URL prefix, e.g. `"https://flagcdn.com/w40/"`.
        base_url: String,
        /// URL suffix, e.g. `".png"`.
        suffix: String,
        /// Square image size in logical px.
        image_size: f64,
        /// Corner radius for the image.
        border_radius: f64,
        /// Gap between image and text.
        gap: f64,
    },
    /// Value-driven progress bar (like DaisyUI's `progress`).
    ///
    /// The raw cell value is parsed as `f64` and mapped to a
    /// fraction in `[0, 1]` via `(value - min) / (max - min)`,
    /// clamped to that range. The scene layer draws a track
    /// rectangle plus a fill rectangle scaled by the fraction,
    /// with an optional `"NN%"` label.
    ///
    /// `class_of` is an optional value → class-string callback,
    /// resolved through the registered `ClassResolver` to pick
    /// the fill colour per value (e.g. DaisyUI
    /// `"progress progress-success"`). When `None`, the fill
    /// uses `Theme::progress_fill` (a single themed colour).
    ///
    /// Like [`Custom`](CellFormat::Custom) and
    /// [`Styled`](CellFormat::Styled), `class_of` uses [`Rc`]
    /// (single-threaded) and is cloned via `Rc::clone`.
    ///
    /// # Example
    /// ```ignore
    /// CellFormat::ProgressBar {
    ///     min: 0.0,
    ///     max: 1.0,
    ///     show_label: true,
    ///     class_of: Some(Rc::new(|raw| {
    ///         let v = raw.parse::<f64>().unwrap_or(0.0);
    ///         if v < 0.4 {
    ///             "progress progress-error".into()
    ///         } else if v < 0.7 {
    ///             "progress progress-warning".into()
    ///         } else {
    ///             "progress progress-success".into()
    ///         }
    ///     })),
    /// }
    /// ```
    #[allow(clippy::type_complexity)]
    ProgressBar {
        /// Value mapped to fraction `0.0`.
        min: f64,
        /// Value mapped to fraction `1.0`.
        max: f64,
        /// Overlay the percentage as text (e.g. `"70%"`).
        show_label: bool,
        /// Optional value → class-string mapping for per-value
        /// fill colours. `None` → `Theme::progress_fill`.
        class_of: Option<Rc<dyn Fn(&str) -> String>>,
    },
}

impl Clone for CellFormat {
    fn clone(&self) -> Self {
        match self {
            Self::Number {
                decimal_places,
                thousands_sep,
                decimal_sep,
            } => Self::Number {
                decimal_places: *decimal_places,
                thousands_sep: *thousands_sep,
                decimal_sep: *decimal_sep,
            },
            Self::Percent { decimal_places } => Self::Percent {
                decimal_places: *decimal_places,
            },
            Self::Currency {
                symbol,
                decimal_places,
                thousands_sep,
                symbol_after,
            } => Self::Currency {
                symbol: symbol.clone(),
                decimal_places: *decimal_places,
                thousands_sep: *thousands_sep,
                symbol_after: *symbol_after,
            },
            Self::Boolean {
                true_label,
                false_label,
            } => Self::Boolean {
                true_label: true_label.clone(),
                false_label: false_label.clone(),
            },
            Self::Custom(f) => Self::Custom(Rc::clone(f)),
            Self::Styled(f) => Self::Styled(Rc::clone(f)),
            Self::Image {
                base_url,
                border_radius,
                padding,
            } => Self::Image {
                base_url: base_url.clone(),
                border_radius: *border_radius,
                padding: *padding,
            },
            Self::ImageText {
                base_url,
                suffix,
                image_size,
                border_radius,
                gap,
            } => Self::ImageText {
                base_url: base_url.clone(),
                suffix: suffix.clone(),
                image_size: *image_size,
                border_radius: *border_radius,
                gap: *gap,
            },
            Self::ProgressBar {
                min,
                max,
                show_label,
                class_of,
            } => Self::ProgressBar {
                min: *min,
                max: *max,
                show_label: *show_label,
                class_of: class_of.as_ref().map(Rc::clone),
            },
        }
    }
}

impl std::fmt::Debug for CellFormat {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Number {
                decimal_places,
                thousands_sep,
                decimal_sep,
            } => f
                .debug_struct("Number")
                .field("decimal_places", decimal_places)
                .field("thousands_sep", thousands_sep)
                .field("decimal_sep", decimal_sep)
                .finish(),
            Self::Percent { decimal_places } => f
                .debug_struct("Percent")
                .field("decimal_places", decimal_places)
                .finish(),
            Self::Currency {
                symbol,
                decimal_places,
                thousands_sep,
                symbol_after,
            } => f
                .debug_struct("Currency")
                .field("symbol", symbol)
                .field("decimal_places", decimal_places)
                .field("thousands_sep", thousands_sep)
                .field("symbol_after", symbol_after)
                .finish(),
            Self::Boolean {
                true_label,
                false_label,
            } => f
                .debug_struct("Boolean")
                .field("true_label", true_label)
                .field("false_label", false_label)
                .finish(),
            Self::Custom(_) => f.debug_tuple("Custom").field(&"..").finish(),
            Self::Styled(_) => f.debug_tuple("Styled").field(&"..").finish(),
            Self::Image {
                base_url,
                border_radius,
                padding,
            } => f
                .debug_struct("Image")
                .field("base_url", base_url)
                .field("border_radius", border_radius)
                .field("padding", padding)
                .finish(),
            Self::ImageText {
                base_url,
                suffix,
                image_size,
                border_radius,
                gap,
            } => f
                .debug_struct("ImageText")
                .field("base_url", base_url)
                .field("suffix", suffix)
                .field("image_size", image_size)
                .field("border_radius", border_radius)
                .field("gap", gap)
                .finish(),
            Self::ProgressBar {
                min,
                max,
                show_label,
                class_of,
            } => f
                .debug_struct("ProgressBar")
                .field("min", min)
                .field("max", max)
                .field("show_label", show_label)
                .field("class_of", &class_of.as_ref().map(|_| ".."))
                .finish(),
        }
    }
}

impl CellFormat {
    /// Returns `true` if this format renders a full-cell
    /// image.
    pub fn is_image(&self) -> bool {
        matches!(self, CellFormat::Image { .. })
    }

    /// Returns `true` if this format renders an image +
    /// text side by side.
    pub fn is_image_text(&self) -> bool {
        matches!(self, CellFormat::ImageText { .. })
    }
}

// ── format_cell ─────────────────────────────────────────

/// Format a raw cell string according to `fmt`.
///
/// Returns the raw value unchanged (as fallback) if
/// parsing fails.
pub fn format_cell(raw: &str, fmt: &CellFormat) -> FormattedCell {
    match fmt {
        CellFormat::Number {
            decimal_places,
            thousands_sep,
            decimal_sep,
        } => match raw.parse::<f64>() {
            Ok(v) => {
                let text = format_number(
                    v,
                    *decimal_places,
                    *thousands_sep,
                    *decimal_sep,
                );
                FormattedCell {
                    text,
                    align: Some(CellAlign::Right),
                    ..Default::default()
                }
            }
            Err(_) => FormattedCell {
                text: raw.to_owned(),
                ..Default::default()
            },
        },
        CellFormat::Percent { decimal_places } => match raw.parse::<f64>() {
            Ok(v) => {
                let pct = v * 100.0;
                let text = format!(
                    "{:.prec$}%",
                    pct,
                    prec = *decimal_places as usize,
                );
                FormattedCell {
                    text,
                    align: Some(CellAlign::Right),
                    ..Default::default()
                }
            }
            Err(_) => FormattedCell {
                text: raw.to_owned(),
                ..Default::default()
            },
        },
        CellFormat::Currency {
            symbol,
            decimal_places,
            thousands_sep,
            symbol_after,
        } => match raw.parse::<f64>() {
            Ok(v) => {
                let num =
                    format_number(v, *decimal_places, *thousands_sep, '.');
                let text = if *symbol_after {
                    format!("{num} {symbol}")
                } else {
                    format!("{symbol}{num}")
                };
                FormattedCell {
                    text,
                    align: Some(CellAlign::Right),
                    ..Default::default()
                }
            }
            Err(_) => FormattedCell {
                text: raw.to_owned(),
                ..Default::default()
            },
        },
        CellFormat::Boolean {
            true_label,
            false_label,
        } => {
            let is_true = matches!(
                raw.trim().to_lowercase().as_str(),
                "true" | "1" | "yes"
            );
            FormattedCell {
                text: if is_true {
                    true_label.clone()
                } else {
                    false_label.clone()
                },
                align: Some(CellAlign::Center),
                ..Default::default()
            }
        }
        CellFormat::Custom(cb) => cb(raw),
        // Styled is handled directly by the scene builder;
        // format_cell is not called for this variant.
        CellFormat::Styled(_) => FormattedCell {
            text: raw.to_owned(),
            ..Default::default()
        },
        CellFormat::Image { .. } => FormattedCell {
            text: raw.to_owned(),
            ..Default::default()
        },
        CellFormat::ImageText { .. } => {
            // Extract text after the first space (the
            // image key). The scene builder handles the
            // image primitive separately.
            let text = raw
                .find(' ')
                .map(|i| raw[i + 1..].to_owned())
                .unwrap_or_else(|| raw.to_owned());
            FormattedCell {
                text,
                ..Default::default()
            }
        }
        // ProgressBar is handled directly by the scene builder;
        // format_cell is not called for this variant.
        CellFormat::ProgressBar { .. } => FormattedCell {
            text: raw.to_owned(),
            ..Default::default()
        },
    }
}

/// Format a f64 with fixed decimal places, a configurable
/// decimal separator, and optional thousands grouping.
fn format_number(
    value: f64,
    decimal_places: u8,
    thousands_sep: Option<char>,
    decimal_sep: char,
) -> String {
    let prec = decimal_places as usize;
    let raw = format!("{value:.prec$}");

    // Split on '.' (Rust always uses '.' for f64 display).
    let (int_part, dec_part) = match raw.split_once('.') {
        Some((i, d)) => (i, Some(d)),
        None => (raw.as_str(), None),
    };

    // Insert thousands separator into the integer part.
    let int_formatted = if let Some(sep) = thousands_sep {
        let negative = int_part.starts_with('-');
        let digits = if negative { &int_part[1..] } else { int_part };
        let mut result = String::with_capacity(digits.len() + digits.len() / 3);
        for (i, ch) in digits.chars().rev().enumerate() {
            if i > 0 && i % 3 == 0 {
                result.push(sep);
            }
            result.push(ch);
        }
        let grouped: String = result.chars().rev().collect();
        if negative {
            format!("-{grouped}")
        } else {
            grouped
        }
    } else {
        int_part.to_owned()
    };

    match dec_part {
        Some(d) => {
            format!("{int_formatted}{decimal_sep}{d}")
        }
        None => int_formatted,
    }
}

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

    #[test]
    fn format_number_basic() {
        let fmt = CellFormat::Number {
            decimal_places: 2,
            thousands_sep: Some(' '),
            decimal_sep: '.',
        };
        let fc = format_cell("1234.567", &fmt);
        assert_eq!(fc.text, "1 234.57");
        assert_eq!(fc.align, Some(CellAlign::Right));
    }

    #[test]
    fn format_number_no_thousands_sep() {
        let fmt = CellFormat::Number {
            decimal_places: 1,
            thousands_sep: None,
            decimal_sep: '.',
        };
        let fc = format_cell("9876.54", &fmt);
        assert_eq!(fc.text, "9876.5");
    }

    #[test]
    fn format_number_comma_decimal_sep() {
        let fmt = CellFormat::Number {
            decimal_places: 2,
            thousands_sep: Some('.'),
            decimal_sep: ',',
        };
        let fc = format_cell("1234.5", &fmt);
        assert_eq!(fc.text, "1.234,50");
    }

    #[test]
    fn format_number_invalid_fallback() {
        let fmt = CellFormat::Number {
            decimal_places: 2,
            thousands_sep: Some(' '),
            decimal_sep: '.',
        };
        let fc = format_cell("abc", &fmt);
        assert_eq!(fc.text, "abc");
    }

    #[test]
    fn format_number_zero_decimals() {
        let fmt = CellFormat::Number {
            decimal_places: 0,
            thousands_sep: Some(' '),
            decimal_sep: '.',
        };
        let fc = format_cell("1234.9", &fmt);
        assert_eq!(fc.text, "1 235");
    }

    #[test]
    fn format_percent() {
        let fmt = CellFormat::Percent { decimal_places: 2 };
        let fc = format_cell("0.75", &fmt);
        assert_eq!(fc.text, "75.00%");
        assert_eq!(fc.align, Some(CellAlign::Right));
    }

    #[test]
    fn format_percent_invalid() {
        let fmt = CellFormat::Percent { decimal_places: 1 };
        let fc = format_cell("N/A", &fmt);
        assert_eq!(fc.text, "N/A");
    }

    #[test]
    fn format_currency_before() {
        let fmt = CellFormat::Currency {
            symbol: "$".into(),
            decimal_places: 2,
            thousands_sep: Some(','),
            symbol_after: false,
        };
        let fc = format_cell("42.5", &fmt);
        assert_eq!(fc.text, "$42.50");
        assert_eq!(fc.align, Some(CellAlign::Right));
    }

    #[test]
    fn format_currency_after() {
        let fmt = CellFormat::Currency {
            symbol: "\u{20ac}".into(),
            decimal_places: 2,
            thousands_sep: Some(' '),
            symbol_after: true,
        };
        let fc = format_cell("1234.5", &fmt);
        assert_eq!(fc.text, "1 234.50 \u{20ac}");
    }

    #[test]
    fn format_boolean_true() {
        let fmt = CellFormat::Boolean {
            true_label: "\u{2713}".into(),
            false_label: "\u{2717}".into(),
        };
        assert_eq!(format_cell("true", &fmt).text, "\u{2713}");
        assert_eq!(format_cell("1", &fmt).text, "\u{2713}");
        assert_eq!(format_cell("yes", &fmt).text, "\u{2713}");
        assert_eq!(format_cell("TRUE", &fmt).text, "\u{2713}");
    }

    #[test]
    fn format_boolean_false() {
        let fmt = CellFormat::Boolean {
            true_label: "\u{2713}".into(),
            false_label: "\u{2717}".into(),
        };
        assert_eq!(format_cell("false", &fmt).text, "\u{2717}");
        assert_eq!(format_cell("0", &fmt).text, "\u{2717}");
        assert_eq!(format_cell("no", &fmt).text, "\u{2717}");
        assert_eq!(format_cell("", &fmt).text, "\u{2717}");
    }

    #[test]
    fn format_boolean_alignment() {
        let fmt = CellFormat::Boolean {
            true_label: "Y".into(),
            false_label: "N".into(),
        };
        let fc = format_cell("true", &fmt);
        assert_eq!(fc.align, Some(CellAlign::Center));
    }

    #[test]
    fn format_custom() {
        let fmt = CellFormat::Custom(Rc::new(|raw: &str| FormattedCell {
            text: raw.to_uppercase(),
            ..Default::default()
        }));
        let fc = format_cell("hello", &fmt);
        assert_eq!(fc.text, "HELLO");
    }

    #[test]
    fn format_number_negative() {
        let fmt = CellFormat::Number {
            decimal_places: 2,
            thousands_sep: Some(','),
            decimal_sep: '.',
        };
        let fc = format_cell("-1234.5", &fmt);
        assert_eq!(fc.text, "-1,234.50");
    }

    #[test]
    fn format_image_returns_raw() {
        let fmt = CellFormat::Image {
            base_url: Some("https://example.com/".into()),
            border_radius: 4.0,
            padding: 2.0,
        };
        let fc = format_cell("photo.png", &fmt);
        assert_eq!(fc.text, "photo.png");
    }

    #[test]
    fn cell_format_is_image() {
        let img = CellFormat::Image {
            base_url: None,
            border_radius: 0.0,
            padding: 0.0,
        };
        assert!(img.is_image());
        let num = CellFormat::Number {
            decimal_places: 2,
            thousands_sep: None,
            decimal_sep: '.',
        };
        assert!(!num.is_image());
    }

    #[test]
    fn format_image_text_extracts_label() {
        let fmt = CellFormat::ImageText {
            base_url: "https://cdn.example.com/".into(),
            suffix: ".png".into(),
            image_size: 20.0,
            border_radius: 2.0,
            gap: 6.0,
        };
        let fc = format_cell("FR France", &fmt);
        assert_eq!(fc.text, "France");
    }

    #[test]
    fn format_image_text_no_space_fallback() {
        let fmt = CellFormat::ImageText {
            base_url: String::new(),
            suffix: String::new(),
            image_size: 16.0,
            border_radius: 0.0,
            gap: 4.0,
        };
        let fc = format_cell("US", &fmt);
        assert_eq!(fc.text, "US");
    }

    #[test]
    fn image_format_clone() {
        let fmt = CellFormat::Image {
            base_url: Some("https://cdn.example.com/".into()),
            border_radius: 8.0,
            padding: 3.0,
        };
        let cloned = fmt.clone();
        assert!(matches!(
            cloned,
            CellFormat::Image {
                border_radius,
                padding,
                ..
            } if border_radius == 8.0 && padding == 3.0
        ));
    }

    #[test]
    fn format_number_zero() {
        let fmt = CellFormat::Number {
            decimal_places: 2,
            thousands_sep: Some(' '),
            decimal_sep: '.',
        };
        let fc = format_cell("0", &fmt);
        assert_eq!(fc.text, "0.00");
    }

    #[test]
    fn format_number_very_large() {
        let fmt = CellFormat::Number {
            decimal_places: 2,
            thousands_sep: Some(' '),
            decimal_sep: '.',
        };
        let fc = format_cell("1234567.89", &fmt);
        assert_eq!(fc.text, "1 234 567.89");
    }

    #[test]
    fn format_currency_non_numeric_fallback() {
        let fmt = CellFormat::Currency {
            symbol: "$".into(),
            decimal_places: 2,
            thousands_sep: None,
            symbol_after: false,
        };
        let fc = format_cell("N/A", &fmt);
        assert_eq!(fc.text, "N/A");
    }

    // ── Clone for every variant ───────────────────────────

    #[test]
    fn clone_number() {
        let fmt = CellFormat::Number {
            decimal_places: 3,
            thousands_sep: Some(','),
            decimal_sep: '.',
        };
        let c = fmt.clone();
        assert!(matches!(
            c,
            CellFormat::Number {
                decimal_places: 3,
                thousands_sep: Some(','),
                decimal_sep: '.',
            }
        ));
    }

    #[test]
    fn clone_percent() {
        let fmt = CellFormat::Percent { decimal_places: 1 };
        let c = fmt.clone();
        assert!(matches!(c, CellFormat::Percent { decimal_places: 1 }));
    }

    #[test]
    fn clone_currency() {
        let fmt = CellFormat::Currency {
            symbol: "€".into(),
            decimal_places: 2,
            thousands_sep: Some(' '),
            symbol_after: true,
        };
        let c = fmt.clone();
        assert!(matches!(
            c,
            CellFormat::Currency {
                symbol_after: true,
                decimal_places: 2,
                ..
            }
        ));
        if let CellFormat::Currency { symbol, .. } = c {
            assert_eq!(symbol, "€");
        }
    }

    #[test]
    fn clone_boolean() {
        let fmt = CellFormat::Boolean {
            true_label: "Yes".into(),
            false_label: "No".into(),
        };
        let c = fmt.clone();
        if let CellFormat::Boolean {
            true_label,
            false_label,
        } = c
        {
            assert_eq!(true_label, "Yes");
            assert_eq!(false_label, "No");
        } else {
            panic!("expected Boolean");
        }
    }

    #[test]
    fn clone_custom() {
        let fmt = CellFormat::Custom(Rc::new(|raw: &str| FormattedCell {
            text: raw.to_owned(),
            ..Default::default()
        }));
        let c = fmt.clone();
        // Cloned callback should still work.
        let fc = format_cell("hi", &c);
        assert_eq!(fc.text, "hi");
    }

    #[test]
    fn clone_image_text() {
        let fmt = CellFormat::ImageText {
            base_url: "https://cdn.example.com/".into(),
            suffix: ".png".into(),
            image_size: 24.0,
            border_radius: 4.0,
            gap: 8.0,
        };
        let c = fmt.clone();
        assert!(matches!(
            c,
            CellFormat::ImageText {
                image_size,
                border_radius,
                gap,
                ..
            } if image_size == 24.0 && border_radius == 4.0 && gap == 8.0
        ));
        if let CellFormat::ImageText {
            base_url, suffix, ..
        } = c
        {
            assert_eq!(base_url, "https://cdn.example.com/");
            assert_eq!(suffix, ".png");
        }
    }

    // ── Debug for every variant ───────────────────────────

    #[test]
    fn debug_all_variants() {
        let variants: &[CellFormat] = &[
            CellFormat::Number {
                decimal_places: 2,
                thousands_sep: Some(','),
                decimal_sep: '.',
            },
            CellFormat::Percent { decimal_places: 1 },
            CellFormat::Currency {
                symbol: "$".into(),
                decimal_places: 2,
                thousands_sep: None,
                symbol_after: false,
            },
            CellFormat::Boolean {
                true_label: "Y".into(),
                false_label: "N".into(),
            },
            CellFormat::Custom(Rc::new(|_: &str| FormattedCell::default())),
            CellFormat::Image {
                base_url: None,
                border_radius: 0.0,
                padding: 0.0,
            },
            CellFormat::ImageText {
                base_url: String::new(),
                suffix: String::new(),
                image_size: 16.0,
                border_radius: 0.0,
                gap: 4.0,
            },
        ];
        for v in variants {
            let s = format!("{v:?}");
            assert!(!s.is_empty(), "Debug should not be empty");
        }
    }

    // ── is_image_text ─────────────────────────────────────

    #[test]
    fn is_image_text_true() {
        let fmt = CellFormat::ImageText {
            base_url: String::new(),
            suffix: String::new(),
            image_size: 16.0,
            border_radius: 0.0,
            gap: 4.0,
        };
        assert!(fmt.is_image_text());
        assert!(!fmt.is_image());
    }

    #[test]
    fn is_image_text_false_for_image() {
        let fmt = CellFormat::Image {
            base_url: None,
            border_radius: 0.0,
            padding: 0.0,
        };
        assert!(!fmt.is_image_text());
        assert!(fmt.is_image());
    }

    #[test]
    fn is_image_text_false_for_other_variants() {
        let fmt = CellFormat::Percent { decimal_places: 2 };
        assert!(!fmt.is_image_text());
        assert!(!fmt.is_image());
    }

    #[test]
    fn styled_clone_works() {
        use std::rc::Rc;
        let fmt = CellFormat::Styled(Rc::new(|raw: &str| {
            vec![CellElement {
                text: raw.to_string(),
                class: "badge".into(),
                align: CellAlign::Left,
            }]
        }));
        let cloned = fmt.clone();
        assert!(matches!(cloned, CellFormat::Styled(_)));
    }

    #[test]
    fn styled_debug_does_not_panic() {
        use std::rc::Rc;
        let fmt = CellFormat::Styled(Rc::new(|_: &str| vec![]));
        let _ = format!("{:?}", fmt);
    }

    #[test]
    fn format_cell_styled_returns_raw_text() {
        use std::rc::Rc;
        let fmt = CellFormat::Styled(Rc::new(|_: &str| vec![]));
        let result = format_cell("hello", &fmt);
        assert_eq!(result.text, "hello");
    }

    // ── CellFormat::ProgressBar ───────────────────────────

    #[test]
    fn progress_bar_clone_preserves_fields_and_callback() {
        let fmt = CellFormat::ProgressBar {
            min: 0.0,
            max: 1.0,
            show_label: true,
            class_of: Some(Rc::new(|_raw: &str| {
                "progress progress-success".into()
            })),
        };
        let cloned = fmt.clone();
        match cloned {
            CellFormat::ProgressBar {
                min,
                max,
                show_label,
                class_of,
            } => {
                assert_eq!(min, 0.0);
                assert_eq!(max, 1.0);
                assert!(show_label);
                let cb = class_of.expect("callback");
                assert_eq!(cb("0.9"), "progress progress-success");
            }
            _ => panic!("expected ProgressBar"),
        }
    }

    #[test]
    fn progress_bar_debug_does_not_panic() {
        let with_cb = CellFormat::ProgressBar {
            min: 0.0,
            max: 100.0,
            show_label: false,
            class_of: Some(Rc::new(|_: &str| String::new())),
        };
        let without_cb = CellFormat::ProgressBar {
            min: 0.0,
            max: 100.0,
            show_label: false,
            class_of: None,
        };
        assert!(format!("{with_cb:?}").contains("ProgressBar"));
        assert!(format!("{without_cb:?}").contains("ProgressBar"));
    }

    #[test]
    fn format_cell_progress_bar_returns_raw_text() {
        let fmt = CellFormat::ProgressBar {
            min: 0.0,
            max: 1.0,
            show_label: true,
            class_of: None,
        };
        let result = format_cell("0.42", &fmt);
        assert_eq!(result.text, "0.42");
    }
}
