//! MATLAB-compatible `num2str` builtin with GPU-aware semantics for RunMat.

use regex::Regex;
use runmat_builtins::{CharArray, ComplexTensor, Tensor, Value};
use runmat_macros::runtime_builtin;

use crate::builtins::common::gpu_helpers;
use crate::builtins::common::map_control_flow_with_builtin;
use crate::builtins::common::spec::{
    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
    ReductionNaN, ResidencyPolicy, ShapeRequirements,
};
use crate::builtins::common::tensor;
use crate::builtins::strings::type_resolvers::string_scalar_type;
use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};

const DEFAULT_PRECISION: usize = 15;
const MAX_PRECISION: usize = 52;

fn num2str_flow(message: impl Into<String>) -> RuntimeError {
    build_runtime_error(message).with_builtin("num2str").build()
}

fn remap_num2str_flow(err: RuntimeError) -> RuntimeError {
    map_control_flow_with_builtin(err, "num2str")
}

#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::strings::core::num2str")]
pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
    name: "num2str",
    op_kind: GpuOpKind::Custom("conversion"),
    supported_precisions: &[],
    broadcast: BroadcastSemantics::None,
    provider_hooks: &[],
    constant_strategy: ConstantStrategy::InlineLiteral,
    residency: ResidencyPolicy::GatherImmediately,
    nan_mode: ReductionNaN::Include,
    two_pass_threshold: None,
    workgroup_size: None,
    accepts_nan_mode: false,
    notes: "Always gathers GPU data to host memory before formatting numeric text.",
};

#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::strings::core::num2str")]
pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
    name: "num2str",
    shape: ShapeRequirements::Any,
    constant_strategy: ConstantStrategy::InlineLiteral,
    elementwise: None,
    reduction: None,
    emits_nan: false,
    notes:
        "Conversion builtin; not eligible for fusion and always materialises host character arrays.",
};

#[runtime_builtin(
    name = "num2str",
    category = "strings/core",
    summary = "Convert numeric scalars, vectors, and matrices into MATLAB-style character arrays using general or custom formats.",
    keywords = "num2str,number to string,format,precision",
    examples = "txt = num2str([1 2 3]);",
    type_resolver(string_scalar_type),
    builtin_path = "crate::builtins::strings::core::num2str"
)]
async fn num2str_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
    let gathered = gather_if_needed_async(&value)
        .await
        .map_err(remap_num2str_flow)?;
    let data = extract_numeric_data(gathered).await?;

    let options = parse_options(rest).await?;
    let char_array = format_numeric_data(data, &options)?;
    Ok(Value::CharArray(char_array))
}

struct FormatOptions {
    spec: FormatSpec,
    decimal: char,
}

#[derive(Clone)]
enum FormatSpec {
    General { digits: usize },
    Custom(CustomFormat),
}

#[derive(Clone)]
struct CustomFormat {
    kind: CustomKind,
    width: Option<usize>,
    precision: Option<usize>,
    sign_always: bool,
    left_align: bool,
    zero_pad: bool,
    uppercase: bool,
}

#[derive(Clone, Copy, PartialEq, Eq)]
enum CustomKind {
    Fixed,
    Exponent,
    General,
}

enum NumericData {
    Real {
        data: Vec<f64>,
        rows: usize,
        cols: usize,
    },
    Complex {
        data: Vec<(f64, f64)>,
        rows: usize,
        cols: usize,
    },
}

async fn parse_options(args: Vec<Value>) -> BuiltinResult<FormatOptions> {
    if args.is_empty() {
        return Ok(FormatOptions {
            spec: FormatSpec::General {
                digits: DEFAULT_PRECISION,
            },
            decimal: '.',
        });
    }

    let mut gathered = Vec::with_capacity(args.len());
    for arg in args {
        gathered.push(
            gather_if_needed_async(&arg)
                .await
                .map_err(remap_num2str_flow)?,
        );
    }

    let mut iter = gathered.into_iter();
    let mut spec = FormatSpec::General {
        digits: DEFAULT_PRECISION,
    };
    let mut decimal = '.';

    if let Some(first) = iter.next() {
        if is_local_token(&first)? {
            decimal = detect_decimal_separator(true);
            if iter.next().is_some() {
                return Err(num2str_flow("num2str: too many input arguments"));
            }
            return Ok(FormatOptions { spec, decimal });
        }

        spec = if let Some(digits) = try_extract_precision(&first)? {
            FormatSpec::General { digits }
        } else if let Some(text) = value_to_text(&first) {
            FormatSpec::Custom(parse_custom_format(&text)?)
        } else {
            return Err(num2str_flow(
                "num2str: second argument must be a precision or format string",
            ));
        };
    }

    if let Some(second) = iter.next() {
        if !is_local_token(&second)? {
            return Err(num2str_flow(
                "num2str: expected 'local' as the third argument",
            ));
        }
        decimal = detect_decimal_separator(true);
    }

    if iter.next().is_some() {
        return Err(num2str_flow("num2str: too many input arguments"));
    }

    Ok(FormatOptions { spec, decimal })
}

fn is_local_token(value: &Value) -> BuiltinResult<bool> {
    let Some(text) = value_to_text(value) else {
        return Ok(false);
    };
    Ok(text.trim().eq_ignore_ascii_case("local"))
}

fn try_extract_precision(value: &Value) -> BuiltinResult<Option<usize>> {
    match value {
        Value::Int(i) => {
            let digits = i.to_i64();
            validate_precision(digits)?;
            Ok(Some(digits as usize))
        }
        Value::Num(n) => {
            if !n.is_finite() {
                return Err(num2str_flow("num2str: precision must be finite"));
            }
            let rounded = n.round();
            if (rounded - n).abs() > f64::EPSILON {
                return Err(num2str_flow("num2str: precision must be an integer"));
            }
            validate_precision(rounded as i64)?;
            Ok(Some(rounded as usize))
        }
        Value::Tensor(t) if t.data.len() == 1 => {
            let value = t.data[0];
            if !value.is_finite() {
                return Err(num2str_flow("num2str: precision must be finite"));
            }
            let rounded = value.round();
            if (rounded - value).abs() > f64::EPSILON {
                return Err(num2str_flow("num2str: precision must be an integer"));
            }
            validate_precision(rounded as i64)?;
            Ok(Some(rounded as usize))
        }
        Value::LogicalArray(la) if la.data.len() == 1 => {
            let digits = if la.data[0] != 0 { 1 } else { 0 };
            validate_precision(digits)?;
            Ok(Some(digits as usize))
        }
        Value::Bool(b) => {
            let digits = if *b { 1 } else { 0 };
            Ok(Some(digits))
        }
        _ => Ok(None),
    }
}

fn validate_precision(value: i64) -> BuiltinResult<()> {
    if value < 0 || value > MAX_PRECISION as i64 {
        return Err(num2str_flow(format!(
            "num2str: precision must satisfy 0 <= p <= {MAX_PRECISION}"
        )));
    }
    Ok(())
}

fn value_to_text(value: &Value) -> Option<String> {
    match value {
        Value::String(s) => Some(s.clone()),
        Value::StringArray(sa) if sa.data.len() == 1 => Some(sa.data[0].clone()),
        Value::CharArray(ca) if ca.rows == 1 => Some(ca.data.iter().collect()),
        _ => None,
    }
}

fn detect_decimal_separator(local: bool) -> char {
    if !local {
        return '.';
    }

    if let Ok(custom) = std::env::var("RUNMAT_DECIMAL_SEPARATOR") {
        let trimmed = custom.trim();
        if let Some(ch) = trimmed.chars().next() {
            return ch;
        }
    }

    let locale = std::env::var("LC_NUMERIC")
        .or_else(|_| std::env::var("RUNMAT_LOCALE"))
        .or_else(|_| std::env::var("LANG"))
        .unwrap_or_default()
        .to_lowercase();

    if locale.is_empty() {
        return '.';
    }

    let comma_locales = [
        "af", "bs", "ca", "cs", "da", "de", "el", "es", "eu", "fi", "fr", "gl", "hr", "hu", "id",
        "is", "it", "lt", "lv", "nb", "nl", "pl", "pt", "ro", "ru", "sk", "sl", "sr", "sv", "tr",
        "uk", "vi",
    ];
    let locale_prefix = locale.split(['.', '_', '@']).next().unwrap_or(&locale);
    for prefix in &comma_locales {
        if locale_prefix.starts_with(prefix) {
            return ',';
        }
    }
    '.'
}

fn parse_custom_format(text: &str) -> BuiltinResult<CustomFormat> {
    if !text.starts_with('%') {
        return Err(num2str_flow("num2str: format must start with '%'"));
    }
    if text == "%%" {
        return Err(num2str_flow(
            "num2str: '%' escape is not supported for numeric conversion",
        ));
    }

    static FORMAT_RE: once_cell::sync::Lazy<Regex> = once_cell::sync::Lazy::new(|| {
        Regex::new(r"^%([+\-0]*)(\d+)?(?:\.(\d*))?([fFeEgG])$").expect("format regex")
    });

    let captures = FORMAT_RE.captures(text).ok_or_else(|| {
        num2str_flow("num2str: unsupported format string; expected variants like '%0.3f' or '%.5g'")
    })?;

    let flags = captures.get(1).map(|m| m.as_str()).unwrap_or("");
    let width = captures
        .get(2)
        .map(|m| m.as_str().parse::<usize>().expect("width parse"));
    let precision = captures.get(3).map(|m| {
        if m.as_str().is_empty() {
            0usize
        } else {
            m.as_str().parse::<usize>().expect("precision parse")
        }
    });
    let conversion = captures
        .get(4)
        .map(|m| m.as_str().chars().next().unwrap())
        .unwrap();

    let mut sign_always = false;
    let mut left_align = false;
    let mut zero_pad = false;

    for ch in flags.chars() {
        match ch {
            '+' => sign_always = true,
            '-' => left_align = true,
            '0' => zero_pad = true,
            _ => {
                return Err(num2str_flow(format!(
                    "num2str: unsupported format flag '{}'; only '+', '-', and '0' are supported",
                    ch
                )))
            }
        }
    }

    if let Some(p) = precision {
        if p > MAX_PRECISION {
            return Err(num2str_flow(format!(
                "num2str: precision must satisfy 0 <= p <= {MAX_PRECISION}"
            )));
        }
    }

    let (kind, uppercase) = match conversion {
        'f' => (CustomKind::Fixed, false),
        'F' => (CustomKind::Fixed, true),
        'e' => (CustomKind::Exponent, false),
        'E' => (CustomKind::Exponent, true),
        'g' => (CustomKind::General, false),
        'G' => (CustomKind::General, true),
        _ => unreachable!(),
    };

    Ok(CustomFormat {
        kind,
        width,
        precision,
        sign_always,
        left_align,
        zero_pad,
        uppercase,
    })
}

async fn extract_numeric_data(value: Value) -> BuiltinResult<NumericData> {
    match value {
        Value::Num(n) => Ok(NumericData::Real {
            data: vec![n],
            rows: 1,
            cols: 1,
        }),
        Value::Int(i) => Ok(NumericData::Real {
            data: vec![i.to_f64()],
            rows: 1,
            cols: 1,
        }),
        Value::Bool(b) => Ok(NumericData::Real {
            data: vec![if b { 1.0 } else { 0.0 }],
            rows: 1,
            cols: 1,
        }),
        Value::Tensor(t) => tensor_to_numeric_data(t),
        Value::LogicalArray(la) => {
            let tensor = tensor::logical_to_tensor(&la).map_err(num2str_flow)?;
            tensor_to_numeric_data(tensor)
        }
        Value::Complex(re, im) => Ok(NumericData::Complex {
            data: vec![(re, im)],
            rows: 1,
            cols: 1,
        }),
        Value::ComplexTensor(t) => complex_tensor_to_data(t),
        Value::GpuTensor(handle) => {
            let gathered = gpu_helpers::gather_tensor_async(&handle)
                .await
                .map_err(remap_num2str_flow)?;
            tensor_to_numeric_data(gathered)
        }
        other => Err(num2str_flow(format!(
            "num2str: unsupported input type {:?}; expected numeric or logical values",
            other
        ))),
    }
}

fn tensor_to_numeric_data(tensor: Tensor) -> BuiltinResult<NumericData> {
    if tensor.shape.len() > 2 {
        return Err(num2str_flow(
            "num2str: input must be scalar, vector, or 2-D matrix",
        ));
    }
    let rows = tensor.rows();
    let cols = tensor.cols();
    if rows == 0 || cols == 0 {
        return Ok(NumericData::Real {
            data: tensor.data,
            rows,
            cols,
        });
    }
    Ok(NumericData::Real {
        data: tensor.data,
        rows,
        cols,
    })
}

fn complex_tensor_to_data(tensor: ComplexTensor) -> BuiltinResult<NumericData> {
    if tensor.shape.len() > 2 {
        return Err(num2str_flow(
            "num2str: complex input must be scalar, vector, or 2-D matrix",
        ));
    }
    let rows = tensor.rows;
    let cols = tensor.cols;
    Ok(NumericData::Complex {
        data: tensor.data,
        rows,
        cols,
    })
}

#[derive(Clone)]
struct CellEntry {
    text: String,
    width: usize,
}

fn format_numeric_data(data: NumericData, options: &FormatOptions) -> BuiltinResult<CharArray> {
    match data {
        NumericData::Real { data, rows, cols } => format_real_matrix(&data, rows, cols, options),
        NumericData::Complex { data, rows, cols } => {
            format_complex_matrix(&data, rows, cols, options)
        }
    }
}

fn format_real_matrix(
    data: &[f64],
    rows: usize,
    cols: usize,
    options: &FormatOptions,
) -> BuiltinResult<CharArray> {
    if rows == 0 {
        return CharArray::new(Vec::new(), 0, 0).map_err(|e| num2str_flow(format!("num2str: {e}")));
    }
    if cols == 0 {
        return CharArray::new(Vec::new(), rows, 0)
            .map_err(|e| num2str_flow(format!("num2str: {e}")));
    }

    let mut entries = vec![
        vec![
            CellEntry {
                text: String::new(),
                width: 0
            };
            cols
        ];
        rows
    ];
    let mut col_widths = vec![0usize; cols];

    for (col, width) in col_widths.iter_mut().enumerate() {
        for (row, row_entries) in entries.iter_mut().enumerate() {
            let idx = row + col * rows;
            let value = data.get(idx).copied().unwrap_or(0.0);
            let text = format_real(value, &options.spec, options.decimal);
            let entry_width = text.chars().count();
            row_entries[col] = CellEntry {
                text,
                width: entry_width,
            };
            if entry_width > *width {
                *width = entry_width;
            }
        }
    }

    if cols > 1 {
        for (idx, width) in col_widths.iter_mut().enumerate() {
            if idx > 0 {
                *width += 1;
            }
        }
    }

    let rows_str = assemble_rows(entries, col_widths);
    rows_to_char_array(rows_str)
}

fn format_complex_matrix(
    data: &[(f64, f64)],
    rows: usize,
    cols: usize,
    options: &FormatOptions,
) -> BuiltinResult<CharArray> {
    if rows == 0 {
        return CharArray::new(Vec::new(), 0, 0).map_err(|e| num2str_flow(format!("num2str: {e}")));
    }
    if cols == 0 {
        return CharArray::new(Vec::new(), rows, 0)
            .map_err(|e| num2str_flow(format!("num2str: {e}")));
    }

    let mut entries = vec![
        vec![
            CellEntry {
                text: String::new(),
                width: 0
            };
            cols
        ];
        rows
    ];
    let mut col_widths = vec![0usize; cols];

    for (col, width) in col_widths.iter_mut().enumerate() {
        for (row, row_entries) in entries.iter_mut().enumerate() {
            let idx = row + col * rows;
            let (re, im) = data.get(idx).copied().unwrap_or((0.0, 0.0));
            let text = format_complex(re, im, &options.spec, options.decimal);
            let entry_width = text.chars().count();
            row_entries[col] = CellEntry {
                text,
                width: entry_width,
            };
            if entry_width > *width {
                *width = entry_width;
            }
        }
    }

    if cols > 1 {
        for (idx, width) in col_widths.iter_mut().enumerate() {
            if idx > 0 {
                *width += 1;
            }
        }
    }

    let rows_str = assemble_rows(entries, col_widths);
    rows_to_char_array(rows_str)
}

fn assemble_rows(entries: Vec<Vec<CellEntry>>, col_widths: Vec<usize>) -> Vec<String> {
    entries
        .into_iter()
        .map(|row_entries| {
            row_entries
                .into_iter()
                .enumerate()
                .fold(String::new(), |mut acc, (col, entry)| {
                    if col > 0 {
                        acc.push(' ');
                    }
                    let target = col_widths[col];
                    let pad = target.saturating_sub(entry.width);
                    acc.extend(std::iter::repeat_n(' ', pad));
                    acc.push_str(&entry.text);
                    acc
                })
        })
        .collect()
}

fn rows_to_char_array(rows: Vec<String>) -> BuiltinResult<CharArray> {
    if rows.is_empty() {
        return CharArray::new(Vec::new(), 0, 0).map_err(|e| num2str_flow(format!("num2str: {e}")));
    }
    let row_count = rows.len();
    let col_count = rows
        .iter()
        .map(|row| row.chars().count())
        .max()
        .unwrap_or(0);

    let mut data = Vec::with_capacity(row_count * col_count);
    for row in rows {
        let mut chars: Vec<char> = row.chars().collect();
        if chars.len() < col_count {
            chars.extend(std::iter::repeat_n(' ', col_count - chars.len()));
        }
        data.extend(chars);
    }

    CharArray::new(data, row_count, col_count).map_err(|e| num2str_flow(format!("num2str: {e}")))
}

fn format_real(value: f64, spec: &FormatSpec, decimal: char) -> String {
    let text = match spec {
        FormatSpec::General { digits } => format_general(value, *digits, false),
        FormatSpec::Custom(custom) => format_custom(value, custom),
    };
    apply_decimal_locale(text, decimal)
}

fn format_complex(re: f64, im: f64, spec: &FormatSpec, decimal: char) -> String {
    let real_str = format_real(re, spec, decimal);
    let imag_sign = if im.is_sign_negative() { '-' } else { '+' };
    let abs_im = if im == 0.0 { 0.0 } else { im.abs() };
    let imag_str = format_real(abs_im, spec, decimal);

    if abs_im == 0.0 && !im.is_nan() {
        return real_str;
    }

    if re == 0.0 && !re.is_sign_negative() && !re.is_nan() {
        if im.is_sign_negative() && !im.is_nan() {
            return format!(
                "{}i",
                if imag_str.starts_with('-') {
                    imag_str.clone()
                } else {
                    format!("-{imag_str}")
                }
            );
        }
        return format!("{imag_str}i");
    }

    format!("{real_str} {imag_sign} {imag_str}i")
}

fn format_general(value: f64, digits: usize, uppercase: bool) -> String {
    if value.is_nan() {
        return "NaN".to_string();
    }
    if value.is_infinite() {
        return if value.is_sign_negative() {
            "-Inf".to_string()
        } else {
            "Inf".to_string()
        };
    }
    if value == 0.0 {
        return "0".to_string();
    }

    let sig_digits = digits.max(1);
    let abs_val = value.abs();
    let exp10 = abs_val.log10().floor() as i32;
    let use_scientific = exp10 < -4 || exp10 >= sig_digits as i32;

    if use_scientific {
        let precision = sig_digits.saturating_sub(1);
        let s = if uppercase {
            format!("{:.*E}", precision, value)
        } else {
            format!("{:.*e}", precision, value)
        };
        let marker = if uppercase { 'E' } else { 'e' };
        if let Some(idx) = s.find(marker) {
            let (mantissa, exponent) = s.split_at(idx);
            let mut mant = mantissa.to_string();
            trim_trailing_zeros(&mut mant);
            normalize_negative_zero(&mut mant);
            let mut result = mant;
            result.push_str(exponent);
            return result;
        }
        s
    } else {
        let decimals = if sig_digits as i32 - 1 - exp10 < 0 {
            0
        } else {
            (sig_digits as i32 - 1 - exp10) as usize
        };
        let mut s = format!("{:.*}", decimals, value);
        trim_trailing_zeros(&mut s);
        normalize_negative_zero(&mut s);
        s
    }
}

fn trim_trailing_zeros(text: &mut String) {
    if let Some(dot_pos) = text.find('.') {
        let mut end = text.len();
        while end > dot_pos + 1 && text.as_bytes()[end - 1] == b'0' {
            end -= 1;
        }
        if end > dot_pos && text.as_bytes()[end - 1] == b'.' {
            end -= 1;
        }
        text.truncate(end);
    }
}

fn normalize_negative_zero(text: &mut String) {
    if text.starts_with('-') && text.chars().skip(1).all(|ch| ch == '0') {
        *text = "0".to_string();
    }
}

fn format_custom(value: f64, fmt: &CustomFormat) -> String {
    if value.is_nan() {
        return "NaN".to_string();
    }
    if value.is_infinite() {
        return if value.is_sign_negative() {
            "-Inf".to_string()
        } else {
            "Inf".to_string()
        };
    }

    let precision = fmt.precision.unwrap_or(match fmt.kind {
        CustomKind::Fixed | CustomKind::Exponent => 6,
        CustomKind::General => DEFAULT_PRECISION,
    });

    let mut text = match fmt.kind {
        CustomKind::Fixed => format!("{:.*}", precision, value),
        CustomKind::Exponent => {
            let mut s = format!("{:.*e}", precision, value);
            if fmt.uppercase {
                s = s.to_uppercase();
            }
            s
        }
        CustomKind::General => format_general(value, precision.max(1), fmt.uppercase),
    };

    if fmt.kind != CustomKind::Fixed {
        trim_trailing_zeros(&mut text);
        normalize_negative_zero(&mut text);
    }

    apply_format_flags(text, fmt)
}

fn apply_decimal_locale(text: String, decimal: char) -> String {
    if decimal == '.' {
        return text;
    }
    let mut replaced = false;
    text.chars()
        .map(|ch| {
            if ch == '.' && !replaced {
                replaced = true;
                decimal
            } else {
                ch
            }
        })
        .collect()
}

fn apply_format_flags(mut text: String, fmt: &CustomFormat) -> String {
    if fmt.sign_always && !text.starts_with('-') && !text.starts_with('+') && text != "NaN" {
        text.insert(0, '+');
    }

    let width = fmt.width.unwrap_or(0);
    if width == 0 {
        return text;
    }

    let len = text.chars().count();
    if len >= width {
        return text;
    }

    let pad_count = width - len;
    let pad_char = if fmt.zero_pad && !fmt.left_align {
        '0'
    } else {
        ' '
    };

    if fmt.left_align {
        let mut result = text.clone();
        result.extend(std::iter::repeat_n(' ', pad_count));
        return result;
    }

    if pad_char == '0' && (text.starts_with('+') || text.starts_with('-')) {
        let mut chars = text.chars();
        let sign = chars.next().unwrap();
        let remainder: String = chars.collect();
        let mut result = String::with_capacity(width);
        result.push(sign);
        result.extend(std::iter::repeat_n('0', pad_count));
        result.push_str(&remainder);
        return result;
    }

    let mut result = String::with_capacity(width);
    result.extend(std::iter::repeat_n(' ', pad_count));
    result.push_str(&text);
    result
}

#[cfg(test)]
pub(crate) mod tests {
    use super::*;
    use crate::builtins::common::test_support;
    use runmat_builtins::{ResolveContext, Type};

    fn num2str_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
        futures::executor::block_on(super::num2str_builtin(value, rest))
    }
    use runmat_builtins::{IntValue, LogicalArray, Tensor};

    fn error_message(err: crate::RuntimeError) -> String {
        err.message().to_string()
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn num2str_scalar_default_precision() {
        let value = Value::Num(std::f64::consts::PI);
        let out = num2str_builtin(value, Vec::new()).expect("num2str");
        match out {
            Value::CharArray(ca) => {
                let text: String = ca.data.iter().collect();
                assert_eq!(ca.rows, 1);
                assert!(text.starts_with("3.1415926535897"));
            }
            other => panic!("expected char array, got {other:?}"),
        }
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn num2str_precision_argument() {
        let value = Value::Num(std::f64::consts::PI);
        let out = num2str_builtin(value, vec![Value::Int(IntValue::I32(4))]).expect("num2str");
        match out {
            Value::CharArray(ca) => {
                let text: String = ca.data.iter().collect();
                assert_eq!(text.trim(), "3.142");
            }
            other => panic!("expected char array, got {other:?}"),
        }
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn num2str_matrix_alignment() {
        let tensor =
            Tensor::new(vec![1.0, 78.0, 23.0, 9.0, 456.0, 10.0], vec![2, 3]).expect("tensor");
        let out = num2str_builtin(Value::Tensor(tensor), Vec::new()).expect("num2str");
        match out {
            Value::CharArray(ca) => {
                assert_eq!(ca.rows, 2);
                assert_eq!(ca.cols, 11);
                let rows: Vec<String> = ca
                    .data
                    .chunks(ca.cols)
                    .map(|chunk| chunk.iter().collect())
                    .collect();
                assert_eq!(rows[0], " 1  23  456");
                assert_eq!(rows[1], "78   9   10");
            }
            other => panic!("expected char array, got {other:?}"),
        }
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn num2str_custom_format() {
        let tensor = Tensor::new(vec![1.234, 5.678], vec![1, 2]).expect("tensor");
        let fmt = Value::String("%.2f".to_string());
        let out = num2str_builtin(Value::Tensor(tensor), vec![fmt]).expect("num2str");
        match out {
            Value::CharArray(ca) => {
                let text: String = ca.data.iter().collect();
                assert_eq!(text, "1.23  5.68");
            }
            other => panic!("expected char array, got {other:?}"),
        }
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn num2str_complex_values() {
        let complex = ComplexTensor::new(vec![(3.0, 4.0), (5.0, -6.0)], vec![1, 2]).expect("cplx");
        let out = num2str_builtin(Value::ComplexTensor(complex), Vec::new()).expect("num2str");
        match out {
            Value::CharArray(ca) => {
                let text: String = ca.data.iter().collect();
                assert_eq!(text, "3 + 4i  5 - 6i");
            }
            other => panic!("expected char array, got {other:?}"),
        }
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn num2str_local_decimal() {
        std::env::set_var("RUNMAT_DECIMAL_SEPARATOR", ",");
        let out =
            num2str_builtin(Value::Num(0.5), vec![Value::String("local".into())]).expect("num2str");
        std::env::remove_var("RUNMAT_DECIMAL_SEPARATOR");
        match out {
            Value::CharArray(ca) => {
                let text: String = ca.data.iter().collect();
                assert_eq!(text, "0,5");
            }
            other => panic!("expected char array, got {other:?}"),
        }
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn num2str_logical_array() {
        let logical = LogicalArray::new(vec![1, 0, 1], vec![1, 3]).expect("logical");
        let out = num2str_builtin(Value::LogicalArray(logical), Vec::new()).expect("num2str");
        match out {
            Value::CharArray(ca) => {
                let text: String = ca.data.iter().collect();
                assert_eq!(text, "1  0  1");
            }
            other => panic!("expected char array, got {other:?}"),
        }
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn num2str_gpu_tensor_roundtrip() {
        test_support::with_test_provider(|provider| {
            let tensor = Tensor::new(vec![10.5, 20.5], vec![1, 2]).expect("tensor");
            let view = runmat_accelerate_api::HostTensorView {
                data: &tensor.data,
                shape: &tensor.shape,
            };
            let handle = provider.upload(&view).expect("upload");
            let out = num2str_builtin(Value::GpuTensor(handle), vec![Value::String("%.1f".into())])
                .expect("num2str");
            match out {
                Value::CharArray(ca) => {
                    let text: String = ca.data.iter().collect();
                    assert_eq!(text, "10.5  20.5");
                }
                other => panic!("expected char array, got {other:?}"),
            }
        });
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn num2str_invalid_input_type() {
        let err =
            error_message(num2str_builtin(Value::String("hello".into()), Vec::new()).unwrap_err());
        assert!(err.contains("unsupported input type"));
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn num2str_invalid_format_string() {
        let err = error_message(
            num2str_builtin(Value::Num(1.0), vec![Value::String("%q".into())]).unwrap_err(),
        );
        assert!(err.contains("unsupported format string"));
    }

    #[test]
    fn num2str_type_is_string_scalar() {
        assert_eq!(
            string_scalar_type(&[Type::Num], &ResolveContext::new(Vec::new())),
            Type::String
        );
    }
}
