//! Date/time functions for JMESPath.
//!
//! This module provides functions for working with dates and times.
//!
//! # Functions
//!
//! | Function | Description |
//! |----------|-------------|
//! | `now()` | Current Unix timestamp in seconds |
//! | `now_millis()` | Current Unix timestamp in milliseconds |
//! | `parse_date(string, format?)` | Parse date string to timestamp |
//! | `format_date(timestamp, format)` | Format timestamp to string |
//! | `date_add(timestamp, amount, unit)` | Add time to timestamp |
//! | `date_diff(ts1, ts2, unit)` | Difference between timestamps |
//! | `timezone_convert(timestamp, from_tz, to_tz)` | Convert timestamp between timezones |
//! | `is_weekend(timestamp)` | Check if date falls on weekend |
//! | `is_weekday(timestamp)` | Check if date falls on weekday |
//! | `business_days_between(ts1, ts2)` | Count business days between dates |
//! | `relative_time(timestamp)` | Human-readable relative time |
//! | `quarter(timestamp)` | Get quarter of year (1-4) |
//! | `is_after(date1, date2)` | Check if date1 is after date2 |
//! | `is_before(date1, date2)` | Check if date1 is before date2 |
//! | `is_between(date, start, end)` | Check if date is between start and end |
//! | `time_ago(date)` | Human-readable time since date |
//!
//! # Format Specifiers
//!
//! Uses [chrono strftime format](https://docs.rs/chrono/latest/chrono/format/strftime/index.html):
//!
//! | Specifier | Description | Example |
//! |-----------|-------------|---------|
//! | `%Y` | Year with century | 2024 |
//! | `%m` | Month (01-12) | 07 |
//! | `%d` | Day of month (01-31) | 15 |
//! | `%H` | Hour (00-23) | 14 |
//! | `%M` | Minute (00-59) | 30 |
//! | `%S` | Second (00-59) | 45 |
//! | `%Y-%m-%d` | ISO date | 2024-07-15 |
//! | `%Y-%m-%dT%H:%M:%S` | ISO datetime | 2024-07-15T14:30:45 |
//!
//! # Time Units
//!
//! For `date_add` and `date_diff`:
//! - `seconds`, `second`, `s`
//! - `minutes`, `minute`, `m`
//! - `hours`, `hour`, `h`
//! - `days`, `day`, `d`
//! - `weeks`, `week`, `w`
//!
//! # Examples
//!
//! ```
//! # #[cfg(feature = "datetime")]
//! # fn main() {
//! use jmespath::{Runtime, Variable};
//! use jmespath_extensions::datetime;
//!
//! let mut runtime = Runtime::new();
//! runtime.register_builtin_functions();
//! datetime::register(&mut runtime);
//!
//! // Format a timestamp
//! let expr = runtime.compile("format_date(`1720000000`, '%Y-%m-%d')").unwrap();
//! let result = expr.search(&Variable::Null).unwrap();
//! assert_eq!(result.as_string().unwrap(), "2024-07-03");
//! # }
//! # #[cfg(not(feature = "datetime"))]
//! # fn main() {}
//! ```

use std::rc::Rc;

use chrono::{DateTime, Datelike, NaiveDateTime, TimeDelta, TimeZone, Utc, Weekday};
use chrono_tz::Tz;

use crate::common::{Function, custom_error};
use crate::{ArgumentType, Context, JmespathError, Rcvar, Runtime, Variable, define_function};

/// Register all datetime functions with the runtime.
pub fn register(runtime: &mut Runtime) {
    runtime.register_function("now", Box::new(NowFn::new()));
    runtime.register_function("now_millis", Box::new(NowMillisFn::new()));
    runtime.register_function("parse_date", Box::new(ParseDateFn::new()));
    runtime.register_function("format_date", Box::new(FormatDateFn::new()));
    runtime.register_function("date_add", Box::new(DateAddFn::new()));
    runtime.register_function("date_diff", Box::new(DateDiffFn::new()));
    runtime.register_function("timezone_convert", Box::new(TimezoneConvertFn::new()));
    runtime.register_function("is_weekend", Box::new(IsWeekendFn::new()));
    runtime.register_function("is_weekday", Box::new(IsWeekdayFn::new()));
    runtime.register_function(
        "business_days_between",
        Box::new(BusinessDaysBetweenFn::new()),
    );
    runtime.register_function("relative_time", Box::new(RelativeTimeFn::new()));
    runtime.register_function("quarter", Box::new(QuarterFn::new()));
    runtime.register_function("is_after", Box::new(IsAfterFn::new()));
    runtime.register_function("is_before", Box::new(IsBeforeFn::new()));
    runtime.register_function("is_between", Box::new(IsBetweenFn::new()));
    runtime.register_function("time_ago", Box::new(TimeAgoFn::new()));
}

// now() -> number
define_function!(NowFn, vec![], None);

impl Function for NowFn {
    fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
        self.signature.validate(args, ctx)?;
        let ts = Utc::now().timestamp();
        Ok(Rc::new(Variable::Number(
            serde_json::Number::from_f64(ts as f64).unwrap(),
        )))
    }
}

// now_millis() -> number
define_function!(NowMillisFn, vec![], None);

impl Function for NowMillisFn {
    fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
        self.signature.validate(args, ctx)?;
        let ts = Utc::now().timestamp_millis();
        Ok(Rc::new(Variable::Number(
            serde_json::Number::from_f64(ts as f64).unwrap(),
        )))
    }
}

// parse_date(string, format?) -> number | null
define_function!(
    ParseDateFn,
    vec![ArgumentType::String],
    Some(ArgumentType::String)
);

impl Function for ParseDateFn {
    fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
        self.signature.validate(args, ctx)?;

        let s = args[0].as_string().unwrap();

        if args.len() > 1 {
            // Custom format provided
            let format = args[1].as_string().unwrap();
            match NaiveDateTime::parse_from_str(s, format) {
                Ok(dt) => Ok(Rc::new(Variable::Number(
                    serde_json::Number::from_f64(dt.and_utc().timestamp() as f64).unwrap(),
                ))),
                Err(_) => Ok(Rc::new(Variable::Null)),
            }
        } else {
            // Try common formats
            if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
                return Ok(Rc::new(Variable::Number(
                    serde_json::Number::from_f64(dt.timestamp() as f64).unwrap(),
                )));
            }
            if let Ok(dt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S") {
                return Ok(Rc::new(Variable::Number(
                    serde_json::Number::from_f64(dt.and_utc().timestamp() as f64).unwrap(),
                )));
            }
            if let Ok(dt) =
                NaiveDateTime::parse_from_str(&format!("{}T00:00:00", s), "%Y-%m-%dT%H:%M:%S")
            {
                return Ok(Rc::new(Variable::Number(
                    serde_json::Number::from_f64(dt.and_utc().timestamp() as f64).unwrap(),
                )));
            }
            Ok(Rc::new(Variable::Null))
        }
    }
}

// format_date(timestamp, format) -> string
define_function!(
    FormatDateFn,
    vec![ArgumentType::Number, ArgumentType::String],
    None
);

impl Function for FormatDateFn {
    fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
        self.signature.validate(args, ctx)?;

        let ts = args[0].as_number().unwrap();
        let format = args[1].as_string().unwrap();

        let dt = Utc.timestamp_opt(ts as i64, 0);
        match dt {
            chrono::LocalResult::Single(dt) => {
                Ok(Rc::new(Variable::String(dt.format(format).to_string())))
            }
            _ => Ok(Rc::new(Variable::Null)),
        }
    }
}

// date_add(timestamp, amount, unit) -> number
define_function!(
    DateAddFn,
    vec![
        ArgumentType::Number,
        ArgumentType::Number,
        ArgumentType::String
    ],
    None
);

impl Function for DateAddFn {
    fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
        self.signature.validate(args, ctx)?;

        let ts = args[0].as_number().unwrap();
        let amount = args[1].as_number().unwrap();
        let unit = args[2].as_string().unwrap();

        let duration = match unit.to_lowercase().as_str() {
            "seconds" | "second" | "s" => TimeDelta::seconds(amount as i64),
            "minutes" | "minute" | "m" => TimeDelta::minutes(amount as i64),
            "hours" | "hour" | "h" => TimeDelta::hours(amount as i64),
            "days" | "day" | "d" => TimeDelta::days(amount as i64),
            "weeks" | "week" | "w" => TimeDelta::weeks(amount as i64),
            _ => return Err(custom_error(ctx, &format!("invalid time unit: {}", unit))),
        };

        let dt = Utc.timestamp_opt(ts as i64, 0);
        match dt {
            chrono::LocalResult::Single(dt) => {
                let new_dt = dt + duration;
                Ok(Rc::new(Variable::Number(
                    serde_json::Number::from_f64(new_dt.timestamp() as f64).unwrap(),
                )))
            }
            _ => Ok(Rc::new(Variable::Null)),
        }
    }
}

// date_diff(ts1, ts2, unit) -> number
define_function!(
    DateDiffFn,
    vec![
        ArgumentType::Number,
        ArgumentType::Number,
        ArgumentType::String
    ],
    None
);

impl Function for DateDiffFn {
    fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
        self.signature.validate(args, ctx)?;

        let ts1 = args[0].as_number().unwrap();
        let ts2 = args[1].as_number().unwrap();
        let unit = args[2].as_string().unwrap();

        let diff_seconds = (ts1 - ts2) as i64;

        let result = match unit.to_lowercase().as_str() {
            "seconds" | "second" | "s" => diff_seconds as f64,
            "minutes" | "minute" | "m" => diff_seconds as f64 / 60.0,
            "hours" | "hour" | "h" => diff_seconds as f64 / 3600.0,
            "days" | "day" | "d" => diff_seconds as f64 / 86400.0,
            "weeks" | "week" | "w" => diff_seconds as f64 / 604800.0,
            _ => return Err(custom_error(ctx, &format!("invalid time unit: {}", unit))),
        };

        Ok(Rc::new(Variable::Number(
            serde_json::Number::from_f64(result).unwrap(),
        )))
    }
}

// timezone_convert(timestamp, from_tz, to_tz) -> string
// Converts a timestamp from one timezone to another and returns ISO format string
define_function!(
    TimezoneConvertFn,
    vec![
        ArgumentType::String,
        ArgumentType::String,
        ArgumentType::String
    ],
    None
);

impl Function for TimezoneConvertFn {
    fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
        self.signature.validate(args, ctx)?;

        let timestamp_str = args[0].as_string().unwrap();
        let from_tz_str = args[1].as_string().unwrap();
        let to_tz_str = args[2].as_string().unwrap();

        // Parse timezone strings
        let from_tz: Tz = from_tz_str
            .parse()
            .map_err(|_| custom_error(ctx, &format!("invalid timezone: {}", from_tz_str)))?;
        let to_tz: Tz = to_tz_str
            .parse()
            .map_err(|_| custom_error(ctx, &format!("invalid timezone: {}", to_tz_str)))?;

        // Parse the input timestamp (try multiple formats)
        let naive_dt =
            if let Ok(dt) = NaiveDateTime::parse_from_str(timestamp_str, "%Y-%m-%dT%H:%M:%S") {
                dt
            } else if let Ok(dt) = NaiveDateTime::parse_from_str(
                &format!("{}T00:00:00", timestamp_str),
                "%Y-%m-%dT%H:%M:%S",
            ) {
                dt
            } else {
                return Err(custom_error(
                    ctx,
                    &format!("invalid timestamp format: {}", timestamp_str),
                ));
            };

        // Interpret the naive datetime in the source timezone
        let from_dt = from_tz
            .from_local_datetime(&naive_dt)
            .single()
            .ok_or_else(|| custom_error(ctx, "ambiguous or invalid local time"))?;

        // Convert to target timezone
        let to_dt = from_dt.with_timezone(&to_tz);

        // Format as ISO string without timezone suffix
        Ok(Rc::new(Variable::String(
            to_dt.format("%Y-%m-%dT%H:%M:%S").to_string(),
        )))
    }
}

// is_weekend(timestamp) -> boolean
// Check if the given timestamp falls on a weekend (Saturday or Sunday)
define_function!(IsWeekendFn, vec![ArgumentType::Number], None);

impl Function for IsWeekendFn {
    fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
        self.signature.validate(args, ctx)?;

        let ts = args[0].as_number().unwrap();
        let dt = Utc.timestamp_opt(ts as i64, 0);

        match dt {
            chrono::LocalResult::Single(dt) => {
                let weekday = dt.weekday();
                let is_weekend = weekday == Weekday::Sat || weekday == Weekday::Sun;
                Ok(Rc::new(Variable::Bool(is_weekend)))
            }
            _ => Ok(Rc::new(Variable::Null)),
        }
    }
}

// is_weekday(timestamp) -> boolean
// Check if the given timestamp falls on a weekday (Monday through Friday)
define_function!(IsWeekdayFn, vec![ArgumentType::Number], None);

impl Function for IsWeekdayFn {
    fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
        self.signature.validate(args, ctx)?;

        let ts = args[0].as_number().unwrap();
        let dt = Utc.timestamp_opt(ts as i64, 0);

        match dt {
            chrono::LocalResult::Single(dt) => {
                let weekday = dt.weekday();
                let is_weekday = weekday != Weekday::Sat && weekday != Weekday::Sun;
                Ok(Rc::new(Variable::Bool(is_weekday)))
            }
            _ => Ok(Rc::new(Variable::Null)),
        }
    }
}

// business_days_between(ts1, ts2) -> number
// Count business days (weekdays) between two timestamps
define_function!(
    BusinessDaysBetweenFn,
    vec![ArgumentType::Number, ArgumentType::Number],
    None
);

impl Function for BusinessDaysBetweenFn {
    fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
        self.signature.validate(args, ctx)?;

        let ts1 = args[0].as_number().unwrap() as i64;
        let ts2 = args[1].as_number().unwrap() as i64;

        let dt1 = match Utc.timestamp_opt(ts1, 0) {
            chrono::LocalResult::Single(dt) => dt,
            _ => return Ok(Rc::new(Variable::Null)),
        };
        let dt2 = match Utc.timestamp_opt(ts2, 0) {
            chrono::LocalResult::Single(dt) => dt,
            _ => return Ok(Rc::new(Variable::Null)),
        };

        // Ensure we iterate from earlier to later date
        let (start, end) = if dt1 <= dt2 {
            (dt1.date_naive(), dt2.date_naive())
        } else {
            (dt2.date_naive(), dt1.date_naive())
        };

        let mut count = 0i64;
        let mut current = start;

        while current < end {
            let weekday = current.weekday();
            if weekday != Weekday::Sat && weekday != Weekday::Sun {
                count += 1;
            }
            current = current.succ_opt().unwrap_or(current);
        }

        // If original order was reversed, return negative count
        let result = if ts1 > ts2 { -count } else { count };

        Ok(Rc::new(Variable::Number(
            serde_json::Number::from_f64(result as f64).unwrap(),
        )))
    }
}

// relative_time(timestamp) -> string
// Returns human-readable relative time (e.g., "2 hours ago", "in 3 days")
define_function!(RelativeTimeFn, vec![ArgumentType::Number], None);

impl Function for RelativeTimeFn {
    fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
        self.signature.validate(args, ctx)?;

        let ts = args[0].as_number().unwrap() as i64;
        let now = Utc::now().timestamp();
        let diff = ts - now;

        let (abs_diff, is_future) = if diff >= 0 {
            (diff, true)
        } else {
            (-diff, false)
        };

        // Determine the unit and value
        let (value, unit_singular, unit_plural) = if abs_diff < 60 {
            (abs_diff, "second", "seconds")
        } else if abs_diff < 3600 {
            (abs_diff / 60, "minute", "minutes")
        } else if abs_diff < 86400 {
            (abs_diff / 3600, "hour", "hours")
        } else if abs_diff < 2592000 {
            (abs_diff / 86400, "day", "days")
        } else if abs_diff < 31536000 {
            (abs_diff / 2592000, "month", "months")
        } else {
            (abs_diff / 31536000, "year", "years")
        };

        let unit = if value == 1 {
            unit_singular
        } else {
            unit_plural
        };
        let result = if is_future {
            format!("in {} {}", value, unit)
        } else {
            format!("{} {} ago", value, unit)
        };

        Ok(Rc::new(Variable::String(result)))
    }
}

// quarter(timestamp) -> number
// Get the quarter of the year (1-4) for the given timestamp
define_function!(QuarterFn, vec![ArgumentType::Number], None);

impl Function for QuarterFn {
    fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
        self.signature.validate(args, ctx)?;

        let ts = args[0].as_number().unwrap();
        let dt = Utc.timestamp_opt(ts as i64, 0);

        match dt {
            chrono::LocalResult::Single(dt) => {
                let month = dt.month();
                let quarter = ((month - 1) / 3) + 1;
                Ok(Rc::new(Variable::Number(
                    serde_json::Number::from_f64(quarter as f64).unwrap(),
                )))
            }
            _ => Ok(Rc::new(Variable::Null)),
        }
    }
}

/// Helper function to parse a date value that can be either a string or a number (timestamp).
/// Returns the Unix timestamp as i64, or None if parsing fails.
fn parse_date_value(value: &Variable) -> Option<i64> {
    match value {
        Variable::Number(n) => n.as_f64().map(|f| f as i64),
        Variable::String(s) => {
            // Try RFC3339 first
            if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
                return Some(dt.timestamp());
            }
            // Try ISO datetime without timezone
            if let Ok(dt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S") {
                return Some(dt.and_utc().timestamp());
            }
            // Try date only
            if let Ok(dt) =
                NaiveDateTime::parse_from_str(&format!("{}T00:00:00", s), "%Y-%m-%dT%H:%M:%S")
            {
                return Some(dt.and_utc().timestamp());
            }
            None
        }
        _ => None,
    }
}

// is_after(date1, date2) -> boolean
// Check if date1 is after date2. Accepts either timestamps (numbers) or date strings.
define_function!(IsAfterFn, vec![ArgumentType::Any, ArgumentType::Any], None);

impl Function for IsAfterFn {
    fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
        self.signature.validate(args, ctx)?;

        let ts1 = parse_date_value(&args[0]);
        let ts2 = parse_date_value(&args[1]);

        match (ts1, ts2) {
            (Some(t1), Some(t2)) => Ok(Rc::new(Variable::Bool(t1 > t2))),
            _ => Ok(Rc::new(Variable::Null)),
        }
    }
}

// is_before(date1, date2) -> boolean
// Check if date1 is before date2. Accepts either timestamps (numbers) or date strings.
define_function!(IsBeforeFn, vec![ArgumentType::Any, ArgumentType::Any], None);

impl Function for IsBeforeFn {
    fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
        self.signature.validate(args, ctx)?;

        let ts1 = parse_date_value(&args[0]);
        let ts2 = parse_date_value(&args[1]);

        match (ts1, ts2) {
            (Some(t1), Some(t2)) => Ok(Rc::new(Variable::Bool(t1 < t2))),
            _ => Ok(Rc::new(Variable::Null)),
        }
    }
}

// is_between(date, start, end) -> boolean
// Check if date is between start and end (inclusive). Accepts either timestamps or date strings.
define_function!(
    IsBetweenFn,
    vec![ArgumentType::Any, ArgumentType::Any, ArgumentType::Any],
    None
);

impl Function for IsBetweenFn {
    fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
        self.signature.validate(args, ctx)?;

        let ts = parse_date_value(&args[0]);
        let start = parse_date_value(&args[1]);
        let end = parse_date_value(&args[2]);

        match (ts, start, end) {
            (Some(t), Some(s), Some(e)) => Ok(Rc::new(Variable::Bool(t >= s && t <= e))),
            _ => Ok(Rc::new(Variable::Null)),
        }
    }
}

// time_ago(date) -> string
// Returns human-readable time since the given date (always in the past tense).
// Unlike relative_time, this always returns "X ago" format even for future dates.
define_function!(TimeAgoFn, vec![ArgumentType::Any], None);

impl Function for TimeAgoFn {
    fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
        self.signature.validate(args, ctx)?;

        let ts = match parse_date_value(&args[0]) {
            Some(t) => t,
            None => return Ok(Rc::new(Variable::Null)),
        };

        let now = Utc::now().timestamp();
        let diff = now - ts;
        let abs_diff = diff.abs();

        // Determine the unit and value
        let (value, unit_singular, unit_plural) = if abs_diff < 60 {
            (abs_diff, "second", "seconds")
        } else if abs_diff < 3600 {
            (abs_diff / 60, "minute", "minutes")
        } else if abs_diff < 86400 {
            (abs_diff / 3600, "hour", "hours")
        } else if abs_diff < 2592000 {
            (abs_diff / 86400, "day", "days")
        } else if abs_diff < 31536000 {
            (abs_diff / 2592000, "month", "months")
        } else {
            (abs_diff / 31536000, "year", "years")
        };

        let unit = if value == 1 {
            unit_singular
        } else {
            unit_plural
        };

        let result = if diff < 0 {
            format!("in {} {}", value, unit)
        } else {
            format!("{} {} ago", value, unit)
        };

        Ok(Rc::new(Variable::String(result)))
    }
}

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

    fn setup() -> Runtime {
        let mut runtime = Runtime::new();
        runtime.register_builtin_functions();
        register(&mut runtime);
        runtime
    }

    #[test]
    fn test_now() {
        let runtime = setup();
        let expr = runtime.compile("now()").unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        let ts = result.as_number().unwrap();
        // Should be a reasonable timestamp (after 2020)
        assert!(ts > 1577836800.0);
    }

    #[test]
    fn test_now_millis() {
        let runtime = setup();
        let expr = runtime.compile("now_millis()").unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        let ts = result.as_number().unwrap();
        // Should be a reasonable timestamp in millis (after 2020)
        assert!(ts > 1577836800000.0);
    }

    #[test]
    fn test_format_date() {
        let runtime = setup();
        // 1720000000 = 2024-07-03T10:26:40Z
        let expr = runtime
            .compile("format_date(`1720000000`, '%Y-%m-%d')")
            .unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert_eq!(result.as_string().unwrap(), "2024-07-03");
    }

    #[test]
    fn test_format_date_with_time() {
        let runtime = setup();
        // Use a known timestamp and verify output format
        let expr = runtime
            .compile("format_date(`0`, '%Y-%m-%dT%H:%M:%S')")
            .unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert_eq!(result.as_string().unwrap(), "1970-01-01T00:00:00");
    }

    #[test]
    fn test_parse_date_iso() {
        let runtime = setup();
        let data = Variable::String("1970-01-01T00:00:00Z".to_string());
        let expr = runtime.compile("parse_date(@)").unwrap();
        let result = expr.search(&data).unwrap();
        assert_eq!(result.as_number().unwrap(), 0.0);
    }

    #[test]
    fn test_parse_date_date_only() {
        let runtime = setup();
        let data = Variable::String("2024-07-03".to_string());
        let expr = runtime.compile("parse_date(@)").unwrap();
        let result = expr.search(&data).unwrap();
        // Should parse as midnight UTC
        assert_eq!(result.as_number().unwrap(), 1719964800.0);
    }

    #[test]
    fn test_parse_date_with_format() {
        let runtime = setup();
        // Use datetime format for custom parsing
        let data = Variable::String("03/07/2024 00:00:00".to_string());
        let expr = runtime
            .compile("parse_date(@, '%d/%m/%Y %H:%M:%S')")
            .unwrap();
        let result = expr.search(&data).unwrap();
        // Should parse as 2024-07-03 midnight UTC
        assert_eq!(result.as_number().unwrap(), 1719964800.0);
    }

    #[test]
    fn test_parse_date_invalid() {
        let runtime = setup();
        let data = Variable::String("not a date".to_string());
        let expr = runtime.compile("parse_date(@)").unwrap();
        let result = expr.search(&data).unwrap();
        assert!(result.is_null());
    }

    #[test]
    fn test_date_add_days() {
        let runtime = setup();
        // Add 7 days to 1720000000
        let expr = runtime
            .compile("date_add(`1720000000`, `7`, 'days')")
            .unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert_eq!(result.as_number().unwrap(), 1720604800.0);
    }

    #[test]
    fn test_date_add_hours() {
        let runtime = setup();
        let expr = runtime
            .compile("date_add(`1720000000`, `24`, 'hours')")
            .unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert_eq!(result.as_number().unwrap(), 1720086400.0);
    }

    #[test]
    fn test_date_add_negative() {
        let runtime = setup();
        // Subtract 1 day
        let expr = runtime
            .compile("date_add(`1720000000`, `-1`, 'day')")
            .unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert_eq!(result.as_number().unwrap(), 1719913600.0);
    }

    #[test]
    fn test_date_diff_days() {
        let runtime = setup();
        // 7 days apart
        let expr = runtime
            .compile("date_diff(`1720604800`, `1720000000`, 'days')")
            .unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert_eq!(result.as_number().unwrap(), 7.0);
    }

    #[test]
    fn test_date_diff_hours() {
        let runtime = setup();
        let expr = runtime
            .compile("date_diff(`1720086400`, `1720000000`, 'hours')")
            .unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert_eq!(result.as_number().unwrap(), 24.0);
    }

    #[test]
    fn test_date_diff_negative() {
        let runtime = setup();
        // Earlier timestamp first
        let expr = runtime
            .compile("date_diff(`1720000000`, `1720604800`, 'days')")
            .unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert_eq!(result.as_number().unwrap(), -7.0);
    }

    #[test]
    fn test_date_add_invalid_unit() {
        let runtime = setup();
        let expr = runtime
            .compile("date_add(`1720000000`, `1`, 'invalid')")
            .unwrap();
        let result = expr.search(&Variable::Null);
        assert!(result.is_err());
    }

    #[test]
    fn test_timezone_convert_ny_to_london() {
        let runtime = setup();
        let data = Variable::String("2024-01-15T10:00:00".to_string());
        let expr = runtime
            .compile("timezone_convert(@, 'America/New_York', 'Europe/London')")
            .unwrap();
        let result = expr.search(&data).unwrap();
        // NY is UTC-5 in January, London is UTC+0, so 10:00 NY = 15:00 London
        assert_eq!(result.as_string().unwrap(), "2024-01-15T15:00:00");
    }

    #[test]
    fn test_timezone_convert_tokyo_to_la() {
        let runtime = setup();
        let data = Variable::String("2024-07-15T09:00:00".to_string());
        let expr = runtime
            .compile("timezone_convert(@, 'Asia/Tokyo', 'America/Los_Angeles')")
            .unwrap();
        let result = expr.search(&data).unwrap();
        // Tokyo is UTC+9, LA is UTC-7 in July (PDT), so 9:00 Tokyo = 17:00 previous day LA
        assert_eq!(result.as_string().unwrap(), "2024-07-14T17:00:00");
    }

    #[test]
    fn test_timezone_convert_invalid_tz() {
        let runtime = setup();
        let data = Variable::String("2024-01-15T10:00:00".to_string());
        let expr = runtime
            .compile("timezone_convert(@, 'Invalid/Zone', 'Europe/London')")
            .unwrap();
        let result = expr.search(&data);
        assert!(result.is_err());
    }

    #[test]
    fn test_is_weekend_saturday() {
        let runtime = setup();
        // 2024-01-13 is a Saturday - timestamp: 1705104000
        let expr = runtime.compile("is_weekend(`1705104000`)").unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert_eq!(result.as_boolean().unwrap(), true);
    }

    #[test]
    fn test_is_weekend_sunday() {
        let runtime = setup();
        // 2024-01-14 is a Sunday - timestamp: 1705190400
        let expr = runtime.compile("is_weekend(`1705190400`)").unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert_eq!(result.as_boolean().unwrap(), true);
    }

    #[test]
    fn test_is_weekend_monday() {
        let runtime = setup();
        // 2024-01-15 is a Monday - timestamp: 1705276800
        let expr = runtime.compile("is_weekend(`1705276800`)").unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert_eq!(result.as_boolean().unwrap(), false);
    }

    #[test]
    fn test_is_weekday_monday() {
        let runtime = setup();
        // 2024-01-15 is a Monday - timestamp: 1705276800
        let expr = runtime.compile("is_weekday(`1705276800`)").unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert_eq!(result.as_boolean().unwrap(), true);
    }

    #[test]
    fn test_is_weekday_saturday() {
        let runtime = setup();
        // 2024-01-13 is a Saturday - timestamp: 1705104000
        let expr = runtime.compile("is_weekday(`1705104000`)").unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert_eq!(result.as_boolean().unwrap(), false);
    }

    #[test]
    fn test_business_days_between() {
        let runtime = setup();
        // 2024-01-01 (Mon) to 2024-01-15 (Mon) - 10 business days
        // ts1: 1704067200 (2024-01-01)
        // ts2: 1705276800 (2024-01-15)
        let expr = runtime
            .compile("business_days_between(`1704067200`, `1705276800`)")
            .unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert_eq!(result.as_number().unwrap(), 10.0);
    }

    #[test]
    fn test_business_days_between_reversed() {
        let runtime = setup();
        // Same dates but reversed - should be negative
        let expr = runtime
            .compile("business_days_between(`1705276800`, `1704067200`)")
            .unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert_eq!(result.as_number().unwrap(), -10.0);
    }

    #[test]
    fn test_business_days_between_same_day() {
        let runtime = setup();
        let expr = runtime
            .compile("business_days_between(`1705276800`, `1705276800`)")
            .unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert_eq!(result.as_number().unwrap(), 0.0);
    }

    #[test]
    fn test_quarter_q1() {
        let runtime = setup();
        // January 15, 2024 - timestamp: 1705276800
        let expr = runtime.compile("quarter(`1705276800`)").unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert_eq!(result.as_number().unwrap(), 1.0);
    }

    #[test]
    fn test_quarter_q2() {
        let runtime = setup();
        // April 15, 2024 - timestamp: 1713139200
        let expr = runtime.compile("quarter(`1713139200`)").unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert_eq!(result.as_number().unwrap(), 2.0);
    }

    #[test]
    fn test_quarter_q3() {
        let runtime = setup();
        // July 15, 2024 - timestamp: 1721001600
        let expr = runtime.compile("quarter(`1721001600`)").unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert_eq!(result.as_number().unwrap(), 3.0);
    }

    #[test]
    fn test_quarter_q4() {
        let runtime = setup();
        // October 15, 2024 - timestamp: 1728950400
        let expr = runtime.compile("quarter(`1728950400`)").unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert_eq!(result.as_number().unwrap(), 4.0);
    }

    #[test]
    fn test_relative_time_past() {
        let runtime = setup();
        // Use a timestamp far in the past (1 year ago)
        let one_year_ago = Utc::now().timestamp() - 31536000;
        let expr_str = format!("relative_time(`{}`)", one_year_ago);
        let expr = runtime.compile(&expr_str).unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert!(result.as_string().unwrap().contains("ago"));
    }

    #[test]
    fn test_relative_time_future() {
        let runtime = setup();
        // Use a timestamp in the future (1 day from now)
        let one_day_future = Utc::now().timestamp() + 86400;
        let expr_str = format!("relative_time(`{}`)", one_day_future);
        let expr = runtime.compile(&expr_str).unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert!(result.as_string().unwrap().starts_with("in "));
    }

    // Tests for is_after

    #[test]
    fn test_is_after_with_timestamps() {
        let runtime = setup();
        // 1720000000 is after 1710000000
        let expr = runtime
            .compile("is_after(`1720000000`, `1710000000`)")
            .unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert_eq!(result.as_boolean().unwrap(), true);
    }

    #[test]
    fn test_is_after_with_timestamps_false() {
        let runtime = setup();
        // 1710000000 is not after 1720000000
        let expr = runtime
            .compile("is_after(`1710000000`, `1720000000`)")
            .unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert_eq!(result.as_boolean().unwrap(), false);
    }

    #[test]
    fn test_is_after_with_date_strings() {
        let runtime = setup();
        let data = Variable::from_json(r#"{"d1": "2024-07-15", "d2": "2024-01-01"}"#).unwrap();
        let expr = runtime.compile("is_after(d1, d2)").unwrap();
        let result = expr.search(&data).unwrap();
        assert_eq!(result.as_boolean().unwrap(), true);
    }

    #[test]
    fn test_is_after_with_iso_strings() {
        let runtime = setup();
        let data =
            Variable::from_json(r#"{"d1": "2024-07-15T10:30:00Z", "d2": "2024-07-15T08:00:00Z"}"#)
                .unwrap();
        let expr = runtime.compile("is_after(d1, d2)").unwrap();
        let result = expr.search(&data).unwrap();
        assert_eq!(result.as_boolean().unwrap(), true);
    }

    #[test]
    fn test_is_after_mixed_types() {
        let runtime = setup();
        // 1720000000 = 2024-07-03T10:26:40Z, which is after 2024-01-01
        let data = Variable::from_json(r#"{"d": "2024-01-01"}"#).unwrap();
        let expr = runtime.compile("is_after(`1720000000`, d)").unwrap();
        let result = expr.search(&data).unwrap();
        assert_eq!(result.as_boolean().unwrap(), true);
    }

    #[test]
    fn test_is_after_equal_dates() {
        let runtime = setup();
        let expr = runtime
            .compile("is_after(`1720000000`, `1720000000`)")
            .unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert_eq!(result.as_boolean().unwrap(), false);
    }

    #[test]
    fn test_is_after_invalid_date() {
        let runtime = setup();
        let data = Variable::from_json(r#"{"d": "not-a-date"}"#).unwrap();
        let expr = runtime.compile("is_after(d, `1720000000`)").unwrap();
        let result = expr.search(&data).unwrap();
        assert!(result.is_null());
    }

    // Tests for is_before

    #[test]
    fn test_is_before_with_timestamps() {
        let runtime = setup();
        // 1710000000 is before 1720000000
        let expr = runtime
            .compile("is_before(`1710000000`, `1720000000`)")
            .unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert_eq!(result.as_boolean().unwrap(), true);
    }

    #[test]
    fn test_is_before_with_timestamps_false() {
        let runtime = setup();
        // 1720000000 is not before 1710000000
        let expr = runtime
            .compile("is_before(`1720000000`, `1710000000`)")
            .unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert_eq!(result.as_boolean().unwrap(), false);
    }

    #[test]
    fn test_is_before_with_date_strings() {
        let runtime = setup();
        let data = Variable::from_json(r#"{"d1": "2024-01-01", "d2": "2024-07-15"}"#).unwrap();
        let expr = runtime.compile("is_before(d1, d2)").unwrap();
        let result = expr.search(&data).unwrap();
        assert_eq!(result.as_boolean().unwrap(), true);
    }

    #[test]
    fn test_is_before_equal_dates() {
        let runtime = setup();
        let expr = runtime
            .compile("is_before(`1720000000`, `1720000000`)")
            .unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert_eq!(result.as_boolean().unwrap(), false);
    }

    // Tests for is_between

    #[test]
    fn test_is_between_with_timestamps_true() {
        let runtime = setup();
        // 1715000000 is between 1710000000 and 1720000000
        let expr = runtime
            .compile("is_between(`1715000000`, `1710000000`, `1720000000`)")
            .unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert_eq!(result.as_boolean().unwrap(), true);
    }

    #[test]
    fn test_is_between_with_timestamps_false() {
        let runtime = setup();
        // 1700000000 is not between 1710000000 and 1720000000
        let expr = runtime
            .compile("is_between(`1700000000`, `1710000000`, `1720000000`)")
            .unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert_eq!(result.as_boolean().unwrap(), false);
    }

    #[test]
    fn test_is_between_with_date_strings() {
        let runtime = setup();
        let data = Variable::from_json(
            r#"{"d": "2024-06-15", "start": "2024-01-01", "end": "2024-12-31"}"#,
        )
        .unwrap();
        let expr = runtime.compile("is_between(d, start, end)").unwrap();
        let result = expr.search(&data).unwrap();
        assert_eq!(result.as_boolean().unwrap(), true);
    }

    #[test]
    fn test_is_between_inclusive_start() {
        let runtime = setup();
        // Date equals start - should be true (inclusive)
        let expr = runtime
            .compile("is_between(`1710000000`, `1710000000`, `1720000000`)")
            .unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert_eq!(result.as_boolean().unwrap(), true);
    }

    #[test]
    fn test_is_between_inclusive_end() {
        let runtime = setup();
        // Date equals end - should be true (inclusive)
        let expr = runtime
            .compile("is_between(`1720000000`, `1710000000`, `1720000000`)")
            .unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert_eq!(result.as_boolean().unwrap(), true);
    }

    #[test]
    fn test_is_between_outside_range() {
        let runtime = setup();
        // Date is after end
        let expr = runtime
            .compile("is_between(`1730000000`, `1710000000`, `1720000000`)")
            .unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert_eq!(result.as_boolean().unwrap(), false);
    }

    // Tests for time_ago

    #[test]
    fn test_time_ago_with_timestamp() {
        let runtime = setup();
        // Use a timestamp 1 hour ago
        let one_hour_ago = Utc::now().timestamp() - 3600;
        let expr_str = format!("time_ago(`{}`)", one_hour_ago);
        let expr = runtime.compile(&expr_str).unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert_eq!(result.as_string().unwrap(), "1 hour ago");
    }

    #[test]
    fn test_time_ago_with_date_string() {
        let runtime = setup();
        // Use a date far in the past (over 1 year)
        let data = Variable::String("2020-01-01".to_string());
        let expr = runtime.compile("time_ago(@)").unwrap();
        let result = expr.search(&data).unwrap();
        assert!(result.as_string().unwrap().contains("years ago"));
    }

    #[test]
    fn test_time_ago_plural() {
        let runtime = setup();
        // Use a timestamp 2 days ago
        let two_days_ago = Utc::now().timestamp() - 172800;
        let expr_str = format!("time_ago(`{}`)", two_days_ago);
        let expr = runtime.compile(&expr_str).unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert_eq!(result.as_string().unwrap(), "2 days ago");
    }

    #[test]
    fn test_time_ago_singular() {
        let runtime = setup();
        // Use a timestamp 1 day ago
        let one_day_ago = Utc::now().timestamp() - 86400;
        let expr_str = format!("time_ago(`{}`)", one_day_ago);
        let expr = runtime.compile(&expr_str).unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert_eq!(result.as_string().unwrap(), "1 day ago");
    }

    #[test]
    fn test_time_ago_future() {
        let runtime = setup();
        // Future dates show "in X"
        let one_day_future = Utc::now().timestamp() + 86400;
        let expr_str = format!("time_ago(`{}`)", one_day_future);
        let expr = runtime.compile(&expr_str).unwrap();
        let result = expr.search(&Variable::Null).unwrap();
        assert!(result.as_string().unwrap().starts_with("in "));
    }

    #[test]
    fn test_time_ago_invalid_date() {
        let runtime = setup();
        let data = Variable::String("not-a-date".to_string());
        let expr = runtime.compile("time_ago(@)").unwrap();
        let result = expr.search(&data).unwrap();
        assert!(result.is_null());
    }
}
