use anyhow::Result;
use jiff::fmt::strtime;
use rust_decimal::Decimal;

use crate::diagnostics::PresentError;
use crate::domain::{InvoiceDocument, InvoiceTotals};
use crate::invoice::{RenderContext, RenderLineItem, RenderParty};
use crate::money::{format_money, format_quantity};

pub fn present(invoice: &InvoiceDocument, totals: &InvoiceTotals) -> Result<RenderContext> {
    let currency = invoice.currency;
    let locale = invoice.locale;
    let fmt = |d: Decimal| format_money(d, currency, locale);

    let date_display = strtime::format(&invoice.date_format, invoice.date).map_err(|source| {
        PresentError::InvalidDateFormat {
            format: invoice.date_format.clone(),
            source,
        }
    })?;

    let items = totals
        .items
        .iter()
        .map(|item| RenderLineItem {
            description: item.description.clone(),
            quantity_display: format_quantity(item.quantity),
            rate_display: fmt(item.rate),
            amount_display: fmt(item.amount),
        })
        .collect();

    let tax_label = if invoice.tax_rate.is_zero() {
        "Tax".to_string()
    } else {
        format!("Tax ({}%)", invoice.tax_rate.normalize())
    };

    let logo_virtual_path = invoice.logo_path.as_ref().map(|p| {
        let ext = p
            .extension()
            .and_then(|e| e.to_str())
            .unwrap_or("svg")
            .to_lowercase();
        format!("/logo.{ext}")
    });

    Ok(RenderContext {
        number: invoice.number.to_string(),
        date_display,
        po_number: invoice.po_number.clone().unwrap_or_default(),
        balance_due_display: fmt(totals.total),
        tax_label,
        tax_note: invoice.tax_note.clone(),
        logo_path: logo_virtual_path,
        sender: RenderParty {
            name: invoice.sender.name.clone(),
            address_lines: split_lines(&invoice.sender.address),
        },
        bill_to_lines: split_lines(&invoice.bill_to),
        ship_to_lines: split_lines(&invoice.ship_to),
        notes_lines: split_lines(invoice.notes.as_deref().unwrap_or("")),
        items,
        subtotal_display: fmt(totals.subtotal),
        tax_display: fmt(totals.tax),
        total_display: fmt(totals.total),
    })
}

fn split_lines(s: &str) -> Vec<String> {
    s.lines()
        .filter(|l| !l.is_empty())
        .map(String::from)
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::currency::Currency;
    use crate::diagnostics::PresentError;
    use crate::domain::{CalculatedLineItem, InvoiceDocument, InvoiceTotals, Party};
    use crate::locale::Locale;
    use jiff::civil::date;
    use rust_decimal_macros::dec;
    use std::path::PathBuf;

    fn base() -> (InvoiceDocument, InvoiceTotals) {
        (
            InvoiceDocument {
                number: 7,
                date: date(2026, 4, 18),
                client: None,
                po_number: None,
                notes: None,
                sender: Party {
                    name: "Me".into(),
                    address: "1 Home\n\nCity".into(),
                },
                bill_to: "Acme\n1 Main".into(),
                ship_to: "".into(),
                items: Vec::new(),
                tax_rate: dec!(0),
                tax_note: None,
                currency: Currency::Eur,
                locale: Locale::EnUs,
                date_format: "%Y-%m-%d".into(),
                logo_path: None,
            },
            InvoiceTotals {
                items: vec![CalculatedLineItem {
                    description: "work".into(),
                    quantity: dec!(2),
                    rate: dec!(100),
                    amount: dec!(200),
                }],
                subtotal: dec!(200),
                tax: dec!(0),
                total: dec!(200),
            },
        )
    }

    #[test]
    fn formats_eur() {
        let (invoice, totals) = base();
        let r = present(&invoice, &totals).unwrap();
        assert_eq!(r.total_display, "€200.00");
        assert_eq!(r.subtotal_display, "€200.00");
        assert_eq!(r.items[0].rate_display, "€100.00");
    }

    #[test]
    fn formats_fi_fi_suffix() {
        let (mut invoice, totals) = base();
        invoice.locale = Locale::FiFi;
        let r = present(&invoice, &totals).unwrap();
        assert_eq!(r.total_display, "200,00\u{00A0}€");
    }

    #[test]
    fn jpy_has_no_decimals() {
        let (mut invoice, mut totals) = base();
        invoice.currency = Currency::Jpy;
        invoice.locale = Locale::JaJp;
        totals.total = dec!(1234.56);
        let r = present(&invoice, &totals).unwrap();
        assert_eq!(r.total_display, "¥1,235");
    }

    #[test]
    fn tax_label_zero_vs_nonzero() {
        let (mut invoice, totals) = base();
        assert_eq!(present(&invoice, &totals).unwrap().tax_label, "Tax");
        invoice.tax_rate = dec!(24);
        assert_eq!(present(&invoice, &totals).unwrap().tax_label, "Tax (24%)");
        invoice.tax_rate = dec!(7.5);
        assert_eq!(present(&invoice, &totals).unwrap().tax_label, "Tax (7.5%)");
    }

    #[test]
    fn date_formatting() {
        let (invoice, totals) = base();
        let r = present(&invoice, &totals).unwrap();
        assert_eq!(r.date_display, "2026-04-18");
    }

    #[test]
    fn multiline_splitting_drops_blanks() {
        let (invoice, totals) = base();
        let r = present(&invoice, &totals).unwrap();
        assert_eq!(r.sender.address_lines, vec!["1 Home", "City"]);
        assert_eq!(r.bill_to_lines, vec!["Acme", "1 Main"]);
        assert!(r.ship_to_lines.is_empty());
        assert!(r.notes_lines.is_empty());
    }

    #[test]
    fn logo_virtual_path_from_extension() {
        let (mut invoice, totals) = base();
        invoice.logo_path = Some(PathBuf::from("/x/y/brand.PNG"));
        let r = present(&invoice, &totals).unwrap();
        assert_eq!(r.logo_path.as_deref(), Some("/logo.png"));
    }

    #[test]
    fn logo_virtual_path_none_when_no_logo() {
        let (invoice, totals) = base();
        let r = present(&invoice, &totals).unwrap();
        assert_eq!(r.logo_path, None);
    }

    #[test]
    fn invalid_date_format_reports_format_string() {
        let (mut invoice, totals) = base();
        invoice.date_format = "%Q".into();
        let err = present(&invoice, &totals).unwrap_err();
        let err = err.downcast::<PresentError>().unwrap();
        assert!(err.to_string().contains("invalid date_format '%Q'"));
    }
}
