//! Clipboard value formatting for CalcPad.
//!
//! Ported from the cross-platform spec in Epic 10-2 (Click-to-Copy Answer).
//! The UI layer decides *when* to copy (single-click vs double-click); this
//! module decides *what* text to produce from a `CalcResult`.
//!
//! # Copy modes
//!
//! | Gesture      | What gets copied             | Example          |
//! |------------- |------------------------------|------------------|
//! | Single-click | Raw numeric value only       | `11.023`         |
//! | Double-click | Display value (with unit)    | `11.023 lbs`     |
//!
//! The engine provides both strings so the platform layer never needs to
//! inspect the result internals.

use crate::types::{CalcResult, CalcValue};

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

/// Pair of clipboard strings derived from a calculation result.
///
/// The UI layer selects which one to copy based on the user gesture.
#[derive(Debug, Clone, PartialEq)]
pub struct ClipboardValues {
    /// Raw numeric value as a string (no unit, no currency symbol).
    /// Used on single-click.
    pub raw_value: String,
    /// Human-readable display string (may include unit or currency symbol).
    /// Used on double-click.
    pub display_value: String,
}

// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------

/// Extract clipboard-ready strings from a `CalcResult`.
///
/// Returns `None` for error results (nothing useful to copy).
///
/// # Examples
///
/// ```
/// use calcpad_engine::types::CalcResult;
/// use calcpad_engine::formatting::clipboard::clipboard_values;
/// use calcpad_engine::span::Span;
///
/// let r = CalcResult::number(42.0, Span::new(0, 2));
/// let cv = clipboard_values(&r).unwrap();
/// assert_eq!(cv.raw_value, "42");
/// assert_eq!(cv.display_value, "42");
///
/// let r = CalcResult::unit_value(11.023, "lbs", Span::new(0, 5));
/// let cv = clipboard_values(&r).unwrap();
/// assert_eq!(cv.raw_value, "11.023");
/// assert_eq!(cv.display_value, "11.023 lbs");
/// ```
pub fn clipboard_values(result: &CalcResult) -> Option<ClipboardValues> {
    match &result.value {
        CalcValue::Number { value } => {
            let raw = format_raw(*value);
            Some(ClipboardValues {
                raw_value: raw.clone(),
                display_value: raw,
            })
        }
        CalcValue::UnitValue { value, unit } => {
            let raw = format_raw(*value);
            let display = format!("{} {}", raw, unit);
            Some(ClipboardValues {
                raw_value: raw,
                display_value: display,
            })
        }
        CalcValue::CurrencyValue { amount, currency } => {
            let raw = format_raw(*amount);
            // Display uses the result's pre-formatted string which includes
            // the currency symbol. Fall back to `raw currency` if display is
            // empty (shouldn't happen in practice).
            let display = if result.metadata.display.is_empty() {
                format!("{} {}", raw, currency)
            } else {
                result.metadata.display.clone()
            };
            Some(ClipboardValues {
                raw_value: raw,
                display_value: display,
            })
        }
        CalcValue::DateTime { date } => Some(ClipboardValues {
            raw_value: date.clone(),
            display_value: date.clone(),
        }),
        CalcValue::TimeDelta { days, description } => Some(ClipboardValues {
            raw_value: days.to_string(),
            display_value: description.clone(),
        }),
        CalcValue::Boolean { value } => {
            let s = value.to_string();
            Some(ClipboardValues {
                raw_value: s.clone(),
                display_value: s,
            })
        }
        CalcValue::Error { .. } => None,
    }
}

// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------

/// Format a raw numeric value for clipboard. Integers are printed without
/// a decimal point; floats keep their natural representation.
fn format_raw(val: f64) -> String {
    if val == val.floor() && val.abs() < 1e15 {
        format!("{}", val as i64)
    } else {
        format!("{}", val)
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::span::Span;
    use crate::types::CalcResult;

    fn sp() -> Span {
        Span::new(0, 1)
    }

    // ── Numbers ──────────────────────────────────────────────────────

    #[test]
    fn number_integer() {
        let r = CalcResult::number(42.0, sp());
        let cv = clipboard_values(&r).unwrap();
        assert_eq!(cv.raw_value, "42");
        assert_eq!(cv.display_value, "42");
    }

    #[test]
    fn number_decimal() {
        let r = CalcResult::number(3.14, sp());
        let cv = clipboard_values(&r).unwrap();
        assert_eq!(cv.raw_value, "3.14");
        assert_eq!(cv.display_value, "3.14");
    }

    #[test]
    fn number_negative() {
        let r = CalcResult::number(-100.0, sp());
        let cv = clipboard_values(&r).unwrap();
        assert_eq!(cv.raw_value, "-100");
        assert_eq!(cv.display_value, "-100");
    }

    #[test]
    fn number_zero() {
        let r = CalcResult::number(0.0, sp());
        let cv = clipboard_values(&r).unwrap();
        assert_eq!(cv.raw_value, "0");
        assert_eq!(cv.display_value, "0");
    }

    // ── Unit values ──────────────────────────────────────────────────

    #[test]
    fn unit_value_single_click_raw() {
        let r = CalcResult::unit_value(11.023, "lbs", sp());
        let cv = clipboard_values(&r).unwrap();
        assert_eq!(cv.raw_value, "11.023");
    }

    #[test]
    fn unit_value_double_click_display() {
        let r = CalcResult::unit_value(11.023, "lbs", sp());
        let cv = clipboard_values(&r).unwrap();
        assert_eq!(cv.display_value, "11.023 lbs");
    }

    #[test]
    fn unit_value_integer_amount() {
        let r = CalcResult::unit_value(5.0, "kg", sp());
        let cv = clipboard_values(&r).unwrap();
        assert_eq!(cv.raw_value, "5");
        assert_eq!(cv.display_value, "5 kg");
    }

    // ── Currency values ──────────────────────────────────────────────

    #[test]
    fn currency_raw_is_numeric() {
        let r = CalcResult::currency_value(1234.56, "USD", sp());
        let cv = clipboard_values(&r).unwrap();
        assert_eq!(cv.raw_value, "1234.56");
    }

    #[test]
    fn currency_display_has_symbol() {
        let r = CalcResult::currency_value(1234.56, "USD", sp());
        let cv = clipboard_values(&r).unwrap();
        // Display comes from CalcResult::currency_value which formats as "$1234.56"
        assert!(cv.display_value.contains("1234.56"));
    }

    #[test]
    fn currency_euro() {
        let r = CalcResult::currency_value(99.99, "EUR", sp());
        let cv = clipboard_values(&r).unwrap();
        assert_eq!(cv.raw_value, "99.99");
        assert!(cv.display_value.contains("99.99"));
    }

    // ── DateTime ─────────────────────────────────────────────────────

    #[test]
    fn datetime_both_same() {
        use chrono::NaiveDate;
        let r = CalcResult::datetime(
            NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
            sp(),
        );
        let cv = clipboard_values(&r).unwrap();
        assert_eq!(cv.raw_value, "2024-01-15");
        assert_eq!(cv.display_value, "2024-01-15");
    }

    // ── TimeDelta ────────────────────────────────────────────────────

    #[test]
    fn time_delta() {
        let r = CalcResult::time_delta(30, "30 days", sp());
        let cv = clipboard_values(&r).unwrap();
        assert_eq!(cv.raw_value, "30");
        assert_eq!(cv.display_value, "30 days");
    }

    // ── Boolean ──────────────────────────────────────────────────────

    #[test]
    fn boolean_true() {
        let r = CalcResult::boolean(true, sp());
        let cv = clipboard_values(&r).unwrap();
        assert_eq!(cv.raw_value, "true");
        assert_eq!(cv.display_value, "true");
    }

    #[test]
    fn boolean_false() {
        let r = CalcResult::boolean(false, sp());
        let cv = clipboard_values(&r).unwrap();
        assert_eq!(cv.raw_value, "false");
        assert_eq!(cv.display_value, "false");
    }

    // ── Errors ───────────────────────────────────────────────────────

    #[test]
    fn error_returns_none() {
        let r = CalcResult::error("Division by zero", sp());
        assert!(clipboard_values(&r).is_none());
    }

    // ── Spec alignment: raw vs display divergence ────────────────────

    #[test]
    fn raw_and_display_differ_for_units() {
        let r = CalcResult::unit_value(42.5, "km", sp());
        let cv = clipboard_values(&r).unwrap();
        assert_ne!(cv.raw_value, cv.display_value);
        assert_eq!(cv.raw_value, "42.5");
        assert_eq!(cv.display_value, "42.5 km");
    }

    #[test]
    fn raw_and_display_same_for_plain_number() {
        let r = CalcResult::number(42.0, sp());
        let cv = clipboard_values(&r).unwrap();
        assert_eq!(cv.raw_value, cv.display_value);
    }
}
