use dreport_core::models::*;
use serde_json::Value;
use std::collections::HashMap;

// Re-export HasOptionalBinding for convenience
pub use dreport_core::models::HasOptionalBinding;

/// Şu anki tarihi verilen format string'ine göre formatla.
/// Desteklenen tokenlar: YYYY, MM, DD, HH, mm, ss
/// WASM'da js_sys::Date, native'de SystemTime kullanır.
fn format_current_date(fmt: &str) -> String {
    let (year, month, day, hour, minute, second) = current_datetime_parts();
    fmt.replace("YYYY", &format!("{:04}", year))
        .replace("MM", &format!("{:02}", month))
        .replace("DD", &format!("{:02}", day))
        .replace("HH", &format!("{:02}", hour))
        .replace("mm", &format!("{:02}", minute))
        .replace("ss", &format!("{:02}", second))
}

#[cfg(target_arch = "wasm32")]
fn current_datetime_parts() -> (i32, u32, u32, u32, u32, u32) {
    let d = js_sys::Date::new_0();
    (
        d.get_full_year() as i32,
        d.get_month() as u32 + 1, // JS months are 0-based
        d.get_date() as u32,
        d.get_hours() as u32,
        d.get_minutes() as u32,
        d.get_seconds() as u32,
    )
}

#[cfg(not(target_arch = "wasm32"))]
fn current_datetime_parts() -> (i32, u32, u32, u32, u32, u32) {
    use std::time::{SystemTime, UNIX_EPOCH};
    let secs = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs();
    // Simple UTC date calculation (no timezone dependency)
    let days = (secs / 86400) as i64;
    let time_of_day = secs % 86400;
    let hour = (time_of_day / 3600) as u32;
    let minute = ((time_of_day % 3600) / 60) as u32;
    let second = (time_of_day % 60) as u32;

    // Days since 1970-01-01 → year/month/day (civil calendar)
    // Algorithm from Howard Hinnant's chrono-compatible date library
    let z = days + 719468;
    let era = (if z >= 0 { z } else { z - 146096 }) / 146097;
    let doe = (z - era * 146097) as u32;
    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
    let y = yoe as i64 + era * 400;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    let mp = (5 * doy + 2) / 153;
    let d = doy - (153 * mp + 2) / 5 + 1;
    let m = if mp < 10 { mp + 3 } else { mp - 9 };
    let y = if m <= 2 { y + 1 } else { y };

    (y as i32, m, d, hour, minute, second)
}

/// Çözümlenmiş rich text span'ı
#[derive(Debug, Clone)]
pub struct ResolvedRichSpan {
    pub text: String,
    pub font_size: Option<f64>,
    pub font_weight: Option<String>,
    pub font_family: Option<String>,
    pub color: Option<String>,
}

/// Çözümlenmiş chart verisi
#[derive(Debug, Clone)]
pub struct ResolvedChartData {
    pub chart_type: ChartType,
    pub categories: Vec<String>,
    pub series: Vec<ChartSeries>,
    pub title: Option<ChartTitle>,
    pub legend: Option<ChartLegend>,
    pub labels: Option<ChartLabels>,
    pub axis: Option<ChartAxis>,
    pub style: ChartStyle,
    pub group_mode: Option<GroupMode>,
}

#[derive(Debug, Clone)]
pub struct ChartSeries {
    pub name: String,
    pub values: Vec<f64>,
}

/// Her element ID'si için çözümlenmiş text içeriğini tutar.
/// Table ve barcode gibi özel tipler de burada çözülür.
#[derive(Debug, Clone)]
pub struct ResolvedData {
    /// element_id → çözümlenmiş text içeriği
    pub texts: HashMap<String, String>,
    /// element_id → çözümlenmiş tablo verileri (headers, rows)
    pub tables: HashMap<String, ResolvedTable>,
    /// element_id → çözümlenmiş barcode değeri
    pub barcodes: HashMap<String, String>,
    /// element_id → çözümlenmiş image src
    pub images: HashMap<String, String>,
    /// page_number element_id → format string (sayfa bölme sonrası çözülecek)
    pub page_number_formats: HashMap<String, String>,
    /// element_id → çözümlenmiş rich text span listesi
    pub rich_texts: HashMap<String, Vec<ResolvedRichSpan>>,
    /// element_id → çözümlenmiş chart verisi
    pub charts: HashMap<String, ResolvedChartData>,
    /// Koşulu sağlamayan (gizlenmesi gereken) element ID'leri
    pub hidden_elements: std::collections::HashSet<String>,
}

#[derive(Debug, Clone)]
pub struct ResolvedTable {
    pub rows: Vec<Vec<String>>,
}

/// JSON path ile veri çek: "firma.unvan" → data["firma"]["unvan"]
fn resolve_path<'a>(data: &'a Value, path: &str) -> &'a Value {
    let mut current = data;
    for key in path.split('.') {
        current = match current {
            Value::Object(map) => map.get(key).unwrap_or(&Value::Null),
            _ => &Value::Null,
        };
    }
    current
}

/// JSON Value → display string
fn value_to_string(v: &Value) -> String {
    match v {
        Value::String(s) => s.clone(),
        Value::Number(n) => n.to_string(),
        Value::Bool(b) => b.to_string(),
        Value::Null => String::new(),
        _ => v.to_string(),
    }
}

/// Template'deki tüm binding'leri çözümle.
pub fn resolve_template(template: &Template, data: &Value) -> ResolvedData {
    let mut resolved = ResolvedData {
        texts: HashMap::new(),
        tables: HashMap::new(),
        barcodes: HashMap::new(),
        images: HashMap::new(),
        page_number_formats: HashMap::new(),
        rich_texts: HashMap::new(),
        charts: HashMap::new(),
        hidden_elements: std::collections::HashSet::new(),
    };
    let fc = template.effective_format_config();
    if let Some(ref header) = template.header {
        resolve_element(
            &TemplateElement::Container(header.clone()),
            data,
            &mut resolved,
            &fc,
        );
    }
    resolve_element(
        &TemplateElement::Container(template.root.clone()),
        data,
        &mut resolved,
        &fc,
    );
    if let Some(ref footer) = template.footer {
        resolve_element(
            &TemplateElement::Container(footer.clone()),
            data,
            &mut resolved,
            &fc,
        );
    }
    resolved
}

/// Koşul değerlendirme: Condition struct'ındaki path, operator, value ile data'yı karşılaştır.
fn evaluate_condition(condition: &dreport_core::models::Condition, data: &Value) -> bool {
    let actual = resolve_path(data, &condition.path);
    match condition.operator.as_str() {
        "empty" => matches!(actual, Value::Null) || actual.as_str().is_some_and(|s| s.is_empty()),
        "not_empty" => !matches!(actual, Value::Null) && !actual.as_str().is_some_and(|s| s.is_empty()),
        "eq" => {
            if let Some(ref expected) = condition.value {
                json_values_eq(actual, expected)
            } else {
                actual.is_null()
            }
        }
        "neq" => {
            if let Some(ref expected) = condition.value {
                !json_values_eq(actual, expected)
            } else {
                !actual.is_null()
            }
        }
        op @ ("gt" | "gte" | "lt" | "lte") => {
            let a = actual.as_f64().unwrap_or(0.0);
            let b = condition.value.as_ref().and_then(|v| v.as_f64()).unwrap_or(0.0);
            match op {
                "gt" => a > b,
                "gte" => a >= b,
                "lt" => a < b,
                "lte" => a <= b,
                _ => unreachable!(),
            }
        }
        _ => true, // bilinmeyen operator → göster
    }
}

/// İki JSON değerini karşılaştır (tip dönüşümlü).
fn json_values_eq(a: &Value, b: &Value) -> bool {
    match (a, b) {
        (Value::Number(a), Value::Number(b)) => a.as_f64() == b.as_f64(),
        (Value::String(a), Value::String(b)) => a == b,
        (Value::Bool(a), Value::Bool(b)) => a == b,
        (Value::Null, Value::Null) => true,
        // Çapraz tip karşılaştırma: sayı string vs sayı
        (Value::String(s), Value::Number(n)) | (Value::Number(n), Value::String(s)) => {
            s.parse::<f64>().ok() == n.as_f64()
        }
        _ => a == b,
    }
}

/// Çözümle optional binding: binding varsa data'dan, yoksa static value'dan
fn resolve_optional_binding(el: &impl HasOptionalBinding, data: &Value) -> String {
    if let Some(binding) = el.binding() {
        value_to_string(resolve_path(data, &binding.path))
    } else {
        el.static_value().unwrap_or_default().to_string()
    }
}

fn resolve_element(el: &TemplateElement, data: &Value, resolved: &mut ResolvedData, format_config: &dreport_core::models::FormatConfig) {
    // Koşul kontrolü: condition varsa ve sağlanmıyorsa, hidden olarak işaretle ve çık
    if let Some(condition) = el.condition() && !evaluate_condition(condition, data) {
        resolved.hidden_elements.insert(el.id().to_string());
        return;
    }

    match el {
        TemplateElement::StaticText(e) => {
            resolved.texts.insert(e.base.id.clone(), e.content.clone());
        }
        TemplateElement::Text(e) => {
            let bound_value = value_to_string(resolve_path(data, &e.binding.path));
            let text = match &e.content {
                Some(prefix) if !prefix.is_empty() => format!("{}{}", prefix, bound_value),
                _ => bound_value,
            };
            resolved.texts.insert(e.base.id.clone(), text);
        }
        TemplateElement::PageNumber(e) => {
            // Format string'i sakla — sayfa bölme sonrası gerçek değerlerle çözülecek
            let fmt = e
                .format
                .as_deref()
                .unwrap_or("{current} / {total}")
                .to_string();
            resolved
                .page_number_formats
                .insert(e.base.id.clone(), fmt.clone());
            // Placeholder koy (tek sayfalık fallback)
            resolved.texts.insert(
                e.base.id.clone(),
                fmt.replace("{current}", "1").replace("{total}", "1"),
            );
        }
        TemplateElement::Barcode(e) => {
            resolved.barcodes.insert(e.base.id.clone(), resolve_optional_binding(e, data));
        }
        TemplateElement::Image(e) => {
            resolved.images.insert(e.base.id.clone(), resolve_optional_binding(e, data));
        }
        TemplateElement::RepeatingTable(e) => {
            let array = resolve_path(data, &e.data_source.path);
            let rows = match array {
                Value::Array(items) => {
                    items
                        .iter()
                        .map(|item| {
                            e.columns
                                .iter()
                                .map(|col| {
                                    let v = resolve_path(item, &col.field);
                                    let raw = value_to_string(v);
                                    // Sütun formatı varsa uygula (currency, percentage, number, date)
                                    if let Some(ref fmt) = col.format {
                                        crate::expr_eval::apply_format_with_config(&raw, Some(fmt.as_str()), format_config)
                                    } else {
                                        raw
                                    }
                                })
                                .collect()
                        })
                        .collect()
                }
                _ => vec![],
            };
            resolved.tables.insert(e.base.id.clone(), ResolvedTable { rows });
        }
        TemplateElement::Container(e) => {
            for child in &e.children {
                resolve_element(child, data, resolved, format_config);
            }
        }
        TemplateElement::CurrentDate(e) => {
            let fmt = e.format.as_deref().unwrap_or("DD.MM.YYYY");
            let text = format_current_date(fmt);
            resolved.texts.insert(e.base.id.clone(), text);
        }
        TemplateElement::Checkbox(e) => {
            let checked = if let Some(binding) = &e.binding {
                let val = resolve_path(data, &binding.path);
                match val {
                    Value::Bool(b) => *b,
                    Value::Number(n) => n.as_f64().unwrap_or(0.0) != 0.0,
                    Value::String(s) => s == "true" || s == "1",
                    _ => false,
                }
            } else {
                e.checked.unwrap_or(false)
            };
            // Store as "true"/"false" string in texts map
            resolved.texts.insert(e.base.id.clone(), checked.to_string());
        }
        TemplateElement::CalculatedText(e) => {
            let result = crate::expr_eval::evaluate_expression(&e.expression, data);
            let formatted = crate::expr_eval::apply_format_with_config(&result, e.format.as_deref(), format_config);
            // Bos ifade veya hata durumunda placeholder goster — element 0 yukseklige dusmesin
            let text = if formatted.is_empty() {
                " ".to_string()
            } else {
                formatted
            };
            resolved.texts.insert(e.base.id.clone(), text);
        }
        TemplateElement::RichText(e) => {
            let spans: Vec<ResolvedRichSpan> = e
                .content
                .iter()
                .map(|span| {
                    let text = if let Some(ref binding) = span.binding {
                        let bound = value_to_string(resolve_path(data, &binding.path));
                        match &span.text {
                            Some(prefix) if !prefix.is_empty() => format!("{}{}", prefix, bound),
                            _ => bound,
                        }
                    } else {
                        span.text.clone().unwrap_or_default()
                    };
                    ResolvedRichSpan {
                        text,
                        font_size: span.style.font_size.or(e.style.font_size),
                        font_weight: span
                            .style
                            .font_weight
                            .clone()
                            .or(e.style.font_weight.clone()),
                        font_family: span
                            .style
                            .font_family
                            .clone()
                            .or(e.style.font_family.clone()),
                        color: span.style.color.clone().or(e.style.color.clone()),
                    }
                })
                .collect();
            resolved.rich_texts.insert(e.base.id.clone(), spans);
        }
        TemplateElement::Chart(e) => {
            let array = resolve_path(data, &e.data_source.path);
            let chart_data = match array {
                Value::Array(items) if !items.is_empty() => resolve_chart_data(e, items),
                _ => ResolvedChartData {
                    chart_type: e.chart_type.clone(),
                    categories: vec![],
                    series: vec![],
                    title: e.title.clone(),
                    legend: e.legend.clone(),
                    labels: e.labels.clone(),
                    axis: e.axis.clone(),
                    style: e.style.clone(),
                    group_mode: e.group_mode.clone(),
                },
            };
            resolved.charts.insert(e.base.id.clone(), chart_data);
        }
        TemplateElement::Line(_) => {}
        TemplateElement::Shape(_) => {}
        TemplateElement::PageBreak(_) => {}
    }
}

fn resolve_chart_data(e: &ChartElement, items: &[Value]) -> ResolvedChartData {
    let (categories, series) = if let Some(ref group_field) = e.group_field {
        // Grouped: her distinct group değeri bir seri olur
        let mut category_order: Vec<String> = Vec::new();
        let mut category_set = std::collections::HashSet::new();
        let mut group_order: Vec<String> = Vec::new();
        let mut group_set = std::collections::HashSet::new();
        // group_name → (category → value) (birden fazla aynı group+category olursa topla)
        let mut group_data: HashMap<String, HashMap<String, f64>> = HashMap::new();

        for item in items {
            let cat = value_to_string(resolve_path(item, &e.category_field));
            let val = resolve_path(item, &e.value_field).as_f64().unwrap_or(0.0);
            let grp = value_to_string(resolve_path(item, group_field));

            if category_set.insert(cat.clone()) {
                category_order.push(cat.clone());
            }
            if group_set.insert(grp.clone()) {
                group_order.push(grp.clone());
            }
            *group_data.entry(grp).or_default().entry(cat).or_insert(0.0) += val;
        }

        let series = group_order
            .iter()
            .map(|grp| {
                let grp_map = group_data.get(grp).unwrap();
                let values = category_order
                    .iter()
                    .map(|cat| *grp_map.get(cat).unwrap_or(&0.0))
                    .collect();
                ChartSeries {
                    name: grp.clone(),
                    values,
                }
            })
            .collect();

        (category_order, series)
    } else {
        // Tek seri
        let mut categories = Vec::new();
        let mut values = Vec::new();
        for item in items {
            categories.push(value_to_string(resolve_path(item, &e.category_field)));
            values.push(resolve_path(item, &e.value_field).as_f64().unwrap_or(0.0));
        }
        let series = vec![ChartSeries {
            name: e.value_field.clone(),
            values,
        }];
        (categories, series)
    };

    ResolvedChartData {
        chart_type: e.chart_type.clone(),
        categories,
        series,
        title: e.title.clone(),
        legend: e.legend.clone(),
        labels: e.labels.clone(),
        axis: e.axis.clone(),
        style: e.style.clone(),
        group_mode: e.group_mode.clone(),
    }
}

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

    #[test]
    fn test_resolve_path_simple() {
        let data: Value = serde_json::json!({"name": "test"});
        assert_eq!(value_to_string(resolve_path(&data, "name")), "test");
    }

    #[test]
    fn test_resolve_path_nested() {
        let data: Value = serde_json::json!({
            "firma": {
                "unvan": "Acme A.Ş.",
                "vergiNo": "123"
            }
        });
        assert_eq!(
            value_to_string(resolve_path(&data, "firma.unvan")),
            "Acme A.Ş."
        );
        assert_eq!(value_to_string(resolve_path(&data, "firma.vergiNo")), "123");
    }

    #[test]
    fn test_resolve_path_missing() {
        let data: Value = serde_json::json!({"name": "test"});
        let result = resolve_path(&data, "nonexistent.path");
        assert!(result.is_null());
        assert_eq!(value_to_string(result), "");
    }

    #[test]
    fn test_resolve_path_deep_missing() {
        let data: Value = serde_json::json!({"a": {"b": 42}});
        let result = resolve_path(&data, "a.b.c.d");
        assert!(result.is_null());
    }

    #[test]
    fn test_value_to_string_types() {
        assert_eq!(value_to_string(&serde_json::json!("hello")), "hello");
        assert_eq!(value_to_string(&serde_json::json!(42)), "42");
        assert_eq!(value_to_string(&serde_json::json!(3.14)), "3.14");
        assert_eq!(value_to_string(&serde_json::json!(true)), "true");
        assert_eq!(value_to_string(&serde_json::json!(null)), "");
    }

    #[test]
    fn test_resolve_array() {
        let data: Value = serde_json::json!({
            "kalemler": [
                { "adi": "Widget", "tutar": 100 },
                { "adi": "Gadget", "tutar": 200 }
            ]
        });
        let arr = resolve_path(&data, "kalemler");
        assert!(arr.is_array());
        assert_eq!(arr.as_array().unwrap().len(), 2);
    }

    #[test]
    fn test_resolve_template_text_binding() {
        let template = Template {
            id: "t1".to_string(),
            name: "Test".to_string(),
            page: PageSettings {
                width: 210.0,
                height: 297.0,
            },
            fonts: vec![],
            header: None,
            footer: None,
            format_config: None,
            locale: None,
            root: ContainerElement {
                base: ElementBase::flow("root".to_string(), SizeConstraint::default()),
                direction: "column".to_string(),
                gap: 0.0,
                padding: Padding::default(),
                align: "stretch".to_string(),
                justify: "start".to_string(),
                style: ContainerStyle::default(),
                break_inside: "auto".to_string(),
                children: vec![TemplateElement::Text(TextElement {
                    base: ElementBase::flow("el_name".to_string(), SizeConstraint::default()),
                    style: TextStyle::default(),
                    content: None,
                    binding: ScalarBinding {
                        path: "firma.unvan".to_string(),
                    },
                })],
            },
        };

        let data = serde_json::json!({
            "firma": { "unvan": "Acme Teknoloji A.Ş." }
        });

        let resolved = resolve_template(&template, &data);
        assert_eq!(
            resolved.texts.get("el_name").unwrap(),
            "Acme Teknoloji A.Ş."
        );
    }

    #[test]
    fn test_resolve_template_text_with_prefix() {
        let template = Template {
            id: "t1".to_string(),
            name: "Test".to_string(),
            page: PageSettings {
                width: 210.0,
                height: 297.0,
            },
            fonts: vec![],
            header: None,
            footer: None,
            format_config: None,
            locale: None,
            root: ContainerElement {
                base: ElementBase::flow("root".to_string(), SizeConstraint::default()),
                direction: "column".to_string(),
                gap: 0.0,
                padding: Padding::default(),
                align: "stretch".to_string(),
                justify: "start".to_string(),
                style: ContainerStyle::default(),
                break_inside: "auto".to_string(),
                children: vec![TemplateElement::Text(TextElement {
                    base: ElementBase::flow("el_no".to_string(), SizeConstraint::default()),
                    style: TextStyle::default(),
                    content: Some("Fatura No: ".to_string()),
                    binding: ScalarBinding {
                        path: "fatura.no".to_string(),
                    },
                })],
            },
        };

        let data = serde_json::json!({
            "fatura": { "no": "FTR-001" }
        });

        let resolved = resolve_template(&template, &data);
        assert_eq!(resolved.texts.get("el_no").unwrap(), "Fatura No: FTR-001");
    }

    #[test]
    fn test_resolve_template_static_text() {
        let template = Template {
            id: "t1".to_string(),
            name: "Test".to_string(),
            page: PageSettings {
                width: 210.0,
                height: 297.0,
            },
            fonts: vec![],
            header: None,
            footer: None,
            format_config: None,
            locale: None,
            root: ContainerElement {
                base: ElementBase::flow("root".to_string(), SizeConstraint::default()),
                direction: "column".to_string(),
                gap: 0.0,
                padding: Padding::default(),
                align: "stretch".to_string(),
                justify: "start".to_string(),
                style: ContainerStyle::default(),
                break_inside: "auto".to_string(),
                children: vec![TemplateElement::StaticText(StaticTextElement {
                    base: ElementBase::flow("title".to_string(), SizeConstraint::default()),
                    style: TextStyle::default(),
                    content: "FATURA".to_string(),
                })],
            },
        };

        let resolved = resolve_template(&template, &serde_json::json!({}));
        assert_eq!(resolved.texts.get("title").unwrap(), "FATURA");
    }

    #[test]
    fn test_resolve_template_table_binding() {
        let template = Template {
            id: "t1".to_string(),
            name: "Test".to_string(),
            page: PageSettings {
                width: 210.0,
                height: 297.0,
            },
            fonts: vec![],
            header: None,
            footer: None,
            format_config: None,
            locale: None,
            root: ContainerElement {
                base: ElementBase::flow("root".to_string(), SizeConstraint::default()),
                direction: "column".to_string(),
                gap: 0.0,
                padding: Padding::default(),
                align: "stretch".to_string(),
                justify: "start".to_string(),
                style: ContainerStyle::default(),
                break_inside: "auto".to_string(),
                children: vec![TemplateElement::RepeatingTable(RepeatingTableElement {
                    base: ElementBase::flow("tbl".to_string(), SizeConstraint::default()),
                    data_source: ArrayBinding {
                        path: "kalemler".to_string(),
                    },
                    columns: vec![
                        TableColumn {
                            id: "col_adi".to_string(),
                            field: "adi".to_string(),
                            title: "Urun Adi".to_string(),
                            width: SizeValue::Fr { value: 1.0 },
                            align: "left".to_string(),
                            format: None,
                        },
                        TableColumn {
                            id: "col_tutar".to_string(),
                            field: "tutar".to_string(),
                            title: "Tutar".to_string(),
                            width: SizeValue::Fixed { value: 30.0 },
                            align: "right".to_string(),
                            format: None,
                        },
                    ],
                    style: TableStyle::default(),
                    repeat_header: Some(true),
                })],
            },
        };

        let data = serde_json::json!({
            "kalemler": [
                { "adi": "Widget", "tutar": 100 },
                { "adi": "Gadget", "tutar": 200 }
            ]
        });

        let resolved = resolve_template(&template, &data);
        let table = resolved.tables.get("tbl").unwrap();
        assert_eq!(table.rows.len(), 2);
        assert_eq!(table.rows[0], vec!["Widget", "100"]);
        assert_eq!(table.rows[1], vec!["Gadget", "200"]);
    }

    #[test]
    fn test_resolve_template_table_empty_array() {
        let template = Template {
            id: "t1".to_string(),
            name: "Test".to_string(),
            page: PageSettings {
                width: 210.0,
                height: 297.0,
            },
            fonts: vec![],
            header: None,
            footer: None,
            format_config: None,
            locale: None,
            root: ContainerElement {
                base: ElementBase::flow("root".to_string(), SizeConstraint::default()),
                direction: "column".to_string(),
                gap: 0.0,
                padding: Padding::default(),
                align: "stretch".to_string(),
                justify: "start".to_string(),
                style: ContainerStyle::default(),
                break_inside: "auto".to_string(),
                children: vec![TemplateElement::RepeatingTable(RepeatingTableElement {
                    base: ElementBase::flow("tbl".to_string(), SizeConstraint::default()),
                    data_source: ArrayBinding {
                        path: "items".to_string(),
                    },
                    columns: vec![TableColumn {
                        id: "c1".to_string(),
                        field: "name".to_string(),
                        title: "Name".to_string(),
                        width: SizeValue::Fr { value: 1.0 },
                        align: "left".to_string(),
                        format: None,
                    }],
                    style: TableStyle::default(),
                    repeat_header: Some(true),
                })],
            },
        };

        let data = serde_json::json!({ "items": [] });
        let resolved = resolve_template(&template, &data);
        let table = resolved.tables.get("tbl").unwrap();
        assert_eq!(table.rows.len(), 0);
    }

    #[test]
    fn test_resolve_template_missing_binding_path() {
        let template = Template {
            id: "t1".to_string(),
            name: "Test".to_string(),
            page: PageSettings {
                width: 210.0,
                height: 297.0,
            },
            fonts: vec![],
            header: None,
            footer: None,
            format_config: None,
            locale: None,
            root: ContainerElement {
                base: ElementBase::flow("root".to_string(), SizeConstraint::default()),
                direction: "column".to_string(),
                gap: 0.0,
                padding: Padding::default(),
                align: "stretch".to_string(),
                justify: "start".to_string(),
                style: ContainerStyle::default(),
                break_inside: "auto".to_string(),
                children: vec![TemplateElement::Text(TextElement {
                    base: ElementBase::flow("el_missing".to_string(), SizeConstraint::default()),
                    style: TextStyle::default(),
                    content: None,
                    binding: ScalarBinding {
                        path: "does.not.exist".to_string(),
                    },
                })],
            },
        };

        let data = serde_json::json!({});
        let resolved = resolve_template(&template, &data);
        // Missing binding path should resolve to empty string
        assert_eq!(resolved.texts.get("el_missing").unwrap(), "");
    }
}
