//! Date builtin - display or format date and time
//!
//! SECURITY: Format strings are validated before use to prevent panics.
//! Invalid format specifiers result in an error message, not a crash.
//! Additionally, runtime format errors (e.g., timezone unavailable) are
//! caught and return graceful errors.

use std::fmt::Write;

use async_trait::async_trait;
use chrono::format::{Item, StrftimeItems};
use chrono::{DateTime, Duration, Local, NaiveDate, NaiveDateTime, TimeZone, Utc};

use super::{Builtin, Context, resolve_path};
use crate::error::Result;
use crate::interpreter::ExecResult;

/// The date builtin - display or set date and time.
///
/// Usage: date [+FORMAT] [-u] [-R] [-I[TIMESPEC]] [-r FILE]
///
/// Options:
///   +FORMAT  Output date according to FORMAT
///   -u       Display UTC time instead of local time
///   -R       Output RFC 2822 formatted date
///   -I[FMT]  Output ISO 8601 formatted date (FMT: date, hours, minutes, seconds)
///   -r FILE  Display the last modification time of FILE
///
/// FORMAT specifiers:
///   %Y  Year with century (e.g., 2024)
///   %m  Month (01-12)
///   %d  Day of month (01-31)
///   %H  Hour (00-23)
///   %M  Minute (00-59)
///   %S  Second (00-59)
///   %s  Seconds since Unix epoch
///   %N  Nanoseconds (000000000-999999999)
///   %a  Abbreviated weekday name
///   %A  Full weekday name
///   %b  Abbreviated month name
///   %B  Full month name
///   %c  Date and time representation
///   %D  Date as %m/%d/%y
///   %F  Date as %Y-%m-%d
///   %T  Time as %H:%M:%S
///   %n  Newline
///   %t  Tab
///   %%  Literal %
/// THREAT[TM-INF-018]: Supports a fixed epoch to prevent leaking real host time.
pub struct Date {
    /// Fixed UTC epoch for virtualized time. None = use real system clock.
    fixed_epoch: Option<DateTime<Utc>>,
}

impl Date {
    pub fn new() -> Self {
        Self { fixed_epoch: None }
    }

    /// Create a Date builtin with a fixed epoch (for sandboxing).
    pub fn with_fixed_epoch(epoch: DateTime<Utc>) -> Self {
        Self {
            fixed_epoch: Some(epoch),
        }
    }

    fn now(&self) -> DateTime<Utc> {
        self.fixed_epoch.unwrap_or_else(Utc::now)
    }
}

/// Validate a strftime format string.
/// Returns Ok(()) if valid, or an error message describing the issue.
///
/// THREAT[TM-INT-003]: chrono::format() can panic on invalid format specifiers
/// Mitigation: Pre-validate format string and return human-readable error
fn validate_format(format: &str) -> std::result::Result<(), String> {
    // StrftimeItems parses the format string and yields Item::Error for invalid specifiers
    for item in StrftimeItems::new(format) {
        if let Item::Error = item {
            return Err(format!("invalid format string: '{}'", format));
        }
    }
    Ok(())
}

/// Strip surrounding quotes from a string (handles parser bug where
/// `--date="value"` passes literal quotes to the builtin).
fn strip_surrounding_quotes(s: &str) -> &str {
    let s = s.trim();
    if s.len() >= 2
        && ((s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')))
    {
        &s[1..s.len() - 1]
    } else {
        s
    }
}

fn uses_epoch_input(s: &str) -> bool {
    strip_surrounding_quotes(s).starts_with('@')
}

/// Parse a base date expression (no compound modifiers).
fn parse_base_date(s: &str, now: DateTime<Utc>) -> std::result::Result<DateTime<Utc>, String> {
    let lower = s.to_lowercase();

    // Epoch timestamp: @1234567890
    if let Some(epoch_str) = s.strip_prefix('@') {
        let ts: i64 = epoch_str
            .trim()
            .parse()
            .map_err(|_| format!("invalid date '{}'", s))?;
        return DateTime::from_timestamp(ts, 0).ok_or_else(|| format!("invalid date '{}'", s));
    }

    // Special words
    match lower.as_str() {
        "now" => return Ok(now),
        "yesterday" => return Ok(now - Duration::days(1)),
        "tomorrow" => return Ok(now + Duration::days(1)),
        _ => {}
    }

    // Relative: "N unit(s) ago" or "+N unit(s)" or "-N unit(s)"
    if let Some(duration) = parse_relative_date(&lower) {
        return Ok(now + duration);
    }

    // Try ISO-like formats: YYYY-MM-DD HH:MM:SS, YYYY-MM-DD
    if let Ok(dt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") {
        return local_naive_to_utc(dt, s);
    }
    if let Ok(dt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S") {
        return local_naive_to_utc(dt, s);
    }
    if let Ok(d) = NaiveDate::parse_from_str(s, "%Y-%m-%d") {
        let dt = d
            .and_hms_opt(0, 0, 0)
            .ok_or_else(|| format!("invalid date '{}'", s))?;
        return local_naive_to_utc(dt, s);
    }

    // Try "Mon DD, YYYY" format
    if let Ok(d) = NaiveDate::parse_from_str(s, "%b %d, %Y") {
        let dt = d
            .and_hms_opt(0, 0, 0)
            .ok_or_else(|| format!("invalid date '{}'", s))?;
        return local_naive_to_utc(dt, s);
    }

    // Try RFC 2822: "Mon, 06 Apr 2026 12:00:00 +0000"
    // GNU date ignores incorrect day-of-week, so if strict parsing fails we
    // strip the DOW prefix and retry — chrono validates DOW strictly.
    if let Ok(dt) = DateTime::parse_from_rfc2822(s) {
        return Ok(dt.with_timezone(&Utc));
    }
    if let Some((_, rest)) = s.split_once(", ") {
        // Parse the date/time/tz portion directly, bypassing DOW validation.
        if let Ok(dt) = DateTime::parse_from_str(rest.trim(), "%d %b %Y %H:%M:%S %z") {
            return Ok(dt.with_timezone(&Utc));
        }
    }

    // Try RFC 3339 / ISO 8601 with timezone: "2024-01-15T12:00:00+00:00"
    if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
        return Ok(dt.with_timezone(&Utc));
    }

    Err(format!("date: invalid date '{}'", s))
}

/// Parse a date string like GNU date's -d flag.
///
/// Supports simple expressions:
///   "now", "yesterday", "tomorrow", "N days ago", "+N days",
///   "N weeks ago", "N months ago", "N years ago", "N hours ago",
///   "@EPOCH", "YYYY-MM-DD", "YYYY-MM-DD HH:MM:SS"
///
/// Supports compound expressions (base ± modifier):
///   "2024-01-15 + 30 days", "yesterday - 2 hours",
///   "@1700000000 + 1 week", "2024-01-15 - 1 month"
fn parse_date_string(s: &str, now: DateTime<Utc>) -> std::result::Result<DateTime<Utc>, String> {
    let s = strip_surrounding_quotes(s.trim());

    // Try compound expression: <base> [+-] <N unit(s)>
    // Match patterns like "2024-01-15 + 30 days" or "yesterday - 2 hours"
    // Use a regex that splits on ` + ` or ` - ` followed by a number and unit
    let re_compound =
        regex::Regex::new(r"^(.+?)\s+([+-])\s+(\d+)\s+(second|minute|hour|day|week|month|year)s?$")
            .ok();

    if let Some(ref re) = re_compound {
        let lower = s.to_lowercase();
        if let Some(caps) = re.captures(&lower)
            && let Some(base_match) = caps.get(1)
        {
            let sign = if &caps[2] == "-" { -1i64 } else { 1i64 };
            let n: i64 = caps[3].parse().unwrap_or(0);
            let unit = &caps[4];

            // Use original case for base string to handle epoch (@N)
            // and ISO dates correctly.
            let orig_base = s[..base_match.end()].trim();
            if let Ok(base_dt) = parse_base_date(orig_base, now) {
                let offset = unit_duration(unit, sign * n);
                return Ok(base_dt + offset);
            }
        }
    }

    parse_base_date(s, now)
}

fn local_naive_to_utc(
    dt: NaiveDateTime,
    original: &str,
) -> std::result::Result<DateTime<Utc>, String> {
    Local
        .from_local_datetime(&dt)
        .single()
        .or_else(|| Local.from_local_datetime(&dt).earliest())
        .map(|local_dt| local_dt.with_timezone(&Utc))
        .ok_or_else(|| format!("date: invalid date '{}'", original))
}

/// Parse relative date expressions like "30 days ago", "+2 weeks", "-1 month"
fn parse_relative_date(s: &str) -> Option<Duration> {
    // "N unit(s) ago"
    let re_ago =
        regex::Regex::new(r"^(\d+)\s+(second|minute|hour|day|week|month|year)s?\s+ago$").ok()?;
    if let Some(caps) = re_ago.captures(s) {
        let n: i64 = caps[1].parse().ok()?;
        return Some(unit_duration(&caps[2], -n));
    }

    // "+N unit(s)" or "-N unit(s)" or "N unit(s)"
    let re_rel =
        regex::Regex::new(r"^([+-]?)(\d+)\s+(second|minute|hour|day|week|month|year)s?$").ok()?;
    if let Some(caps) = re_rel.captures(s) {
        let sign = if &caps[1] == "-" { -1i64 } else { 1i64 };
        let n: i64 = caps[2].parse().ok()?;
        return Some(unit_duration(&caps[3], sign * n));
    }

    // "next unit" / "last unit"
    if let Some(unit) = s.strip_prefix("next ") {
        let unit = unit.trim().trim_end_matches('s');
        return Some(unit_duration(unit, 1));
    }
    if let Some(unit) = s.strip_prefix("last ") {
        let unit = unit.trim().trim_end_matches('s');
        return Some(unit_duration(unit, -1));
    }

    None
}

fn unit_duration(unit: &str, n: i64) -> Duration {
    match unit {
        "second" => Duration::seconds(n),
        "minute" => Duration::minutes(n),
        "hour" => Duration::hours(n),
        "day" => Duration::days(n),
        "week" => Duration::weeks(n),
        "month" => Duration::days(n * 30), // Approximate
        "year" => Duration::days(n * 365), // Approximate
        _ => Duration::zero(),
    }
}

/// Expand `%N` (nanoseconds) in a format string, replacing it with the
/// zero-padded nanosecond value from the given datetime.
fn expand_nanoseconds(format: &str, nanos: u32) -> String {
    // Replace %N with the 9-digit nanosecond value
    // Must not replace %%N (literal %N)
    let mut result = String::with_capacity(format.len());
    let mut chars = format.chars().peekable();
    while let Some(ch) = chars.next() {
        if ch == '%' {
            match chars.peek() {
                Some(&'%') => {
                    // %% → pass through both (chrono will render as literal %)
                    result.push('%');
                    result.push('%');
                    chars.next();
                }
                Some(&'N') => {
                    chars.next();
                    let _ = write!(result, "{:09}", nanos);
                }
                _ => {
                    result.push('%');
                }
            }
        } else {
            result.push(ch);
        }
    }
    result
}

/// Format an RFC 2822 date string from a UTC datetime.
fn format_rfc2822(dt: &DateTime<Utc>, utc: bool) -> String {
    if utc {
        dt.format("%a, %d %b %Y %H:%M:%S +0000").to_string()
    } else {
        let local_dt: DateTime<Local> = (*dt).into();
        local_dt.format("%a, %d %b %Y %H:%M:%S %z").to_string()
    }
}

/// Format an ISO 8601 date string.
fn format_iso8601(dt: &DateTime<Utc>, utc: bool, precision: &str) -> String {
    match precision {
        "hours" => {
            if utc {
                dt.format("%Y-%m-%dT%H+00:00").to_string()
            } else {
                let local_dt: DateTime<Local> = (*dt).into();
                local_dt.format("%Y-%m-%dT%H%:z").to_string()
            }
        }
        "minutes" => {
            if utc {
                dt.format("%Y-%m-%dT%H:%M+00:00").to_string()
            } else {
                let local_dt: DateTime<Local> = (*dt).into();
                local_dt.format("%Y-%m-%dT%H:%M%:z").to_string()
            }
        }
        "seconds" | "s" => {
            if utc {
                dt.format("%Y-%m-%dT%H:%M:%S+00:00").to_string()
            } else {
                let local_dt: DateTime<Local> = (*dt).into();
                local_dt.format("%Y-%m-%dT%H:%M:%S%:z").to_string()
            }
        }
        // "date" or default
        _ => dt.format("%Y-%m-%d").to_string(),
    }
}

#[async_trait]
impl Builtin for Date {
    async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
        if let Some(r) = super::check_help_version(
            ctx.args,
            "Usage: date [+FORMAT] [-u] [-R] [-I[TIMESPEC]] [-d STRING] [-r FILE]\nDisplay the current time in the given FORMAT, or set the system date.\n\n  +FORMAT\toutput date according to FORMAT\n  -d, --date=STRING\tdisplay time described by STRING\n  -r, --reference=FILE\tdisplay the last modification time of FILE\n  -u, --utc\tprint Coordinated Universal Time (UTC)\n  -R, --rfc-email\toutput RFC 2822 formatted date\n  -I[FMT], --iso-8601[=FMT]\toutput ISO 8601 date/time (FMT: date, hours, minutes, seconds)\n  --help\tdisplay this help and exit\n  --version\toutput version information and exit\n",
            Some("date (bashkit) 0.1"),
        ) {
            return Ok(r);
        }
        let mut utc = false;
        let mut format_arg: Option<String> = None;
        let mut date_str: Option<String> = None;
        let mut ref_file: Option<String> = None;
        let mut rfc2822 = false;
        let mut iso8601: Option<String> = None;

        let mut p = super::arg_parser::ArgParser::new(ctx.args);
        while !p.is_done() {
            if p.flag_any(&["-u", "--utc"]) {
                utc = true;
            } else if let Some(val) = p.current().and_then(|s| s.strip_prefix("--date=")) {
                date_str = Some(strip_surrounding_quotes(val).to_string());
                p.advance();
            } else if let Some(val) = p.flag_value_opt("-d") {
                date_str = Some(val.to_string());
            } else if p.flag("--date") {
                if let Some(val) = p.positional() {
                    date_str = Some(val.to_string());
                }
            } else if let Some(val) = p.current().and_then(|s| s.strip_prefix("--reference=")) {
                ref_file = Some(val.to_string());
                p.advance();
            } else if let Some(val) = p.flag_value_opt("-r") {
                ref_file = Some(val.to_string());
            } else if p.flag("--reference") {
                if let Some(val) = p.positional() {
                    ref_file = Some(val.to_string());
                }
            } else if p.flag_any(&["-R", "--rfc-2822", "--rfc-email"]) {
                rfc2822 = true;
            } else if let Some(val) = p.current().and_then(|s| s.strip_prefix("--iso-8601=")) {
                iso8601 = Some(val.to_string());
                p.advance();
            } else if p.flag_any(&["-I", "--iso-8601"]) {
                iso8601 = Some("date".to_string());
            } else if let Some(val) = p.current().and_then(|s| s.strip_prefix("-I")) {
                iso8601 = Some(val.to_string());
                p.advance();
            } else if let Some(arg) = p.current().filter(|s| s.starts_with('+')) {
                format_arg = Some(arg.to_string());
                p.advance();
            } else {
                p.advance();
            }
        }

        // Get the datetime to format
        // THREAT[TM-INF-018]: Use virtual time if configured
        let now = self.now();

        // Resolve the datetime: -r (file mtime) > -d (date string) > now
        let epoch_input;
        let dt_utc;
        if let Some(ref file) = ref_file {
            // -r / --reference: stat file to get modification time
            // Resolve relative paths against CWD (fix for issue #1225)
            let path = resolve_path(ctx.cwd, file);
            match ctx.fs.stat(&path).await {
                Ok(meta) => {
                    dt_utc = meta.modified.into();
                    epoch_input = false;
                }
                Err(_) => {
                    return Ok(ExecResult::err(
                        format!("date: cannot stat '{}': No such file or directory\n", file),
                        1,
                    ));
                }
            }
        } else if let Some(ref ds) = date_str {
            epoch_input = uses_epoch_input(ds);
            dt_utc = match parse_date_string(ds, now) {
                Ok(dt) => dt,
                Err(e) => return Ok(ExecResult::err(format!("{}\n", e), 1)),
            };
        } else {
            epoch_input = false;
            dt_utc = now;
        };

        // Handle -R (RFC 2822) output
        if rfc2822 {
            let output = format_rfc2822(&dt_utc, utc);
            return Ok(ExecResult::ok(format!("{}\n", output)));
        }

        // Handle -I (ISO 8601) output
        if let Some(ref precision) = iso8601 {
            let output = format_iso8601(&dt_utc, utc, precision);
            return Ok(ExecResult::ok(format!("{}\n", output)));
        }

        let default_format = "%a %b %e %H:%M:%S %Z %Y".to_string();
        let format_owned;
        let format = match &format_arg {
            Some(fmt) => {
                let without_plus = &fmt[1..]; // Strip leading '+'
                format_owned = strip_surrounding_quotes(without_plus).to_string();
                &format_owned
            }
            None => &default_format,
        };

        // Expand %N before chrono validation (chrono doesn't know %N)
        let nanos = dt_utc.timestamp_subsec_nanos();
        let format = expand_nanoseconds(format, nanos);

        // SECURITY: Validate format string before use to prevent panics
        // THREAT[TM-INT-003]: Invalid format strings could cause chrono to panic
        if let Err(e) = validate_format(&format) {
            return Ok(ExecResult {
                stdout: String::new(),
                stderr: format!("date: {}\n", e),
                exit_code: 1,
                control_flow: crate::interpreter::ControlFlow::None,
                ..Default::default()
            });
        }

        // Format the date, handling potential errors gracefully.
        let mut output = String::new();
        let format_result = if utc || epoch_input {
            write!(output, "{}", dt_utc.format(&format))
        } else {
            let local_dt: DateTime<Local> = dt_utc.into();
            write!(output, "{}", local_dt.format(&format))
        };

        match format_result {
            Ok(()) => Ok(ExecResult::ok(format!("{}\n", output))),
            Err(_) => Ok(ExecResult::err(
                format!("date: failed to format date with '{}'\n", format),
                1,
            )),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::HashMap;
    use std::path::PathBuf;
    use std::sync::Arc;

    use crate::fs::InMemoryFs;

    async fn run_date(args: &[&str]) -> ExecResult {
        let fs = Arc::new(InMemoryFs::new());
        let mut variables = HashMap::new();
        let env = HashMap::new();
        let mut cwd = PathBuf::from("/");

        let args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs,
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        Date::new().execute(ctx).await.unwrap()
    }

    #[tokio::test]
    async fn test_date_default() {
        let result = run_date(&[]).await;
        assert_eq!(result.exit_code, 0);
        // Just check it outputs something with a newline
        assert!(result.stdout.ends_with('\n'));
        assert!(result.stdout.len() > 10);
    }

    #[tokio::test]
    async fn test_date_format_year() {
        let result = run_date(&["+%Y"]).await;
        assert_eq!(result.exit_code, 0);
        // Should be a 4-digit year
        let year = result.stdout.trim();
        assert_eq!(year.len(), 4);
        assert!(year.chars().all(|c| c.is_ascii_digit()));
    }

    #[tokio::test]
    async fn test_date_format_iso() {
        let result = run_date(&["+%Y-%m-%d"]).await;
        assert_eq!(result.exit_code, 0);
        // Should be like 2024-01-15
        let date = result.stdout.trim();
        assert_eq!(date.len(), 10);
        assert!(date.chars().nth(4) == Some('-'));
        assert!(date.chars().nth(7) == Some('-'));
    }

    #[tokio::test]
    async fn test_date_epoch() {
        let result = run_date(&["+%s"]).await;
        assert_eq!(result.exit_code, 0);
        // Should be a valid unix timestamp (10 digits or more)
        let epoch = result.stdout.trim();
        assert!(epoch.len() >= 10);
        assert!(epoch.parse::<i64>().is_ok());
    }

    #[tokio::test]
    async fn test_date_utc() {
        let result = run_date(&["-u", "+%Z"]).await;
        assert_eq!(result.exit_code, 0);
        // Should show UTC timezone
        let tz = result.stdout.trim();
        assert!(tz.contains("UTC") || tz == "+0000" || tz == "+00:00");
    }

    #[tokio::test]
    async fn test_date_time_format() {
        let result = run_date(&["+%H:%M:%S"]).await;
        assert_eq!(result.exit_code, 0);
        // Should be like 12:34:56
        let time = result.stdout.trim();
        assert_eq!(time.len(), 8);
        let parts: Vec<&str> = time.split(':').collect();
        assert_eq!(parts.len(), 3);
    }

    // Tests from main: timezone handling
    #[tokio::test]
    async fn test_date_timezone_utc() {
        // %Z with UTC should always work and produce "UTC"
        let result = run_date(&["-u", "+%Z"]).await;
        assert_eq!(result.exit_code, 0);
        let tz = result.stdout.trim();
        assert!(tz.contains("UTC") || tz == "+0000" || tz == "+00:00");
    }

    #[tokio::test]
    async fn test_date_default_format_includes_timezone() {
        // The default format includes %Z - this tests that it doesn't panic
        let result = run_date(&[]).await;
        assert_eq!(result.exit_code, 0);
        // Default format: "%a %b %e %H:%M:%S %Z %Y"
        // Should contain a year
        let output = result.stdout.trim();
        assert!(
            output.len() > 15,
            "Default format should produce substantial output"
        );
    }

    #[tokio::test]
    async fn test_date_timezone_local() {
        // %Z with local time - this is the case that can fail in some environments
        // With our fix, it should either succeed or return a graceful error
        let result = run_date(&["+%Z"]).await;
        // Either succeeds with exit_code 0, or fails gracefully with exit_code 1
        if result.exit_code == 0 {
            // Successful: output should be non-empty
            assert!(!result.stdout.trim().is_empty());
        } else {
            // Failed gracefully: should have error message
            assert!(result.stderr.contains("date:"));
            assert!(result.stderr.contains("failed to format"));
        }
    }

    #[tokio::test]
    async fn test_date_combined_format_with_timezone() {
        // Test combination of formats including %Z
        let result = run_date(&["-u", "+%Y-%m-%d %H:%M:%S %Z"]).await;
        assert_eq!(result.exit_code, 0);
        let output = result.stdout.trim();
        // Should have date, time, and timezone
        assert!(output.contains('-')); // Date separator
        assert!(output.contains(':')); // Time separator
    }

    #[tokio::test]
    async fn test_date_empty_format() {
        // Empty format string (just "+")
        let result = run_date(&["+"]).await;
        assert_eq!(result.exit_code, 0);
        // Should produce just a newline
        assert_eq!(result.stdout, "\n");
    }

    #[tokio::test]
    async fn test_date_literal_text_in_format() {
        // Format with literal text
        let result = run_date(&["+Today is %A"]).await;
        assert_eq!(result.exit_code, 0);
        assert!(result.stdout.starts_with("Today is "));
    }

    // Tests for invalid format validation (TM-INT-003)
    #[tokio::test]
    async fn test_date_invalid_format_specifier() {
        // Invalid format specifier should return error, not panic
        let result = run_date(&["+%Q"]).await;
        assert_eq!(result.exit_code, 1);
        assert!(result.stderr.contains("invalid format string"));
        assert!(result.stdout.is_empty());
    }

    #[tokio::test]
    async fn test_date_incomplete_format_specifier() {
        // Incomplete specifier at end should return error, not panic
        let result = run_date(&["+%Y-%m-%"]).await;
        assert_eq!(result.exit_code, 1);
        assert!(result.stderr.contains("invalid format string"));
    }

    #[tokio::test]
    async fn test_date_mixed_valid_invalid_format() {
        // Mix of valid and invalid should still error
        let result = run_date(&["+%Y-%Q-%d"]).await;
        assert_eq!(result.exit_code, 1);
        assert!(result.stderr.contains("invalid format string"));
    }

    // === Tests for -d / --date flag ===

    #[tokio::test]
    async fn test_date_d_now() {
        let result = run_date(&["-d", "now", "+%Y"]).await;
        assert_eq!(result.exit_code, 0);
        let year = result.stdout.trim();
        assert_eq!(year.len(), 4);
    }

    #[tokio::test]
    async fn test_date_d_yesterday() {
        let result = run_date(&["-d", "yesterday", "+%Y-%m-%d"]).await;
        assert_eq!(result.exit_code, 0);
        let date = result.stdout.trim();
        assert_eq!(date.len(), 10);
    }

    #[tokio::test]
    async fn test_date_d_tomorrow() {
        let result = run_date(&["-d", "tomorrow", "+%Y-%m-%d"]).await;
        assert_eq!(result.exit_code, 0);
        let date = result.stdout.trim();
        assert_eq!(date.len(), 10);
    }

    #[tokio::test]
    async fn test_date_d_days_ago() {
        let result = run_date(&["-d", "30 days ago", "+%Y-%m-%d"]).await;
        assert_eq!(result.exit_code, 0);
        let date = result.stdout.trim();
        assert_eq!(date.len(), 10);
    }

    #[tokio::test]
    async fn test_date_d_epoch() {
        let result = run_date(&["-u", "-d", "@0", "+%Y-%m-%d"]).await;
        assert_eq!(result.exit_code, 0);
        assert_eq!(result.stdout.trim(), "1970-01-01");
    }

    #[tokio::test]
    async fn test_date_d_epoch_defaults_to_utc() {
        let result = run_date(&["-d", "@0", "+%Y-%m-%d"]).await;
        assert_eq!(result.exit_code, 0);
        assert_eq!(result.stdout.trim(), "1970-01-01");
    }

    #[tokio::test]
    async fn test_date_d_iso_date() {
        let result = run_date(&["-d", "2024-01-15", "+%Y-%m-%d"]).await;
        assert_eq!(result.exit_code, 0);
        assert_eq!(result.stdout.trim(), "2024-01-15");
    }

    #[tokio::test]
    async fn test_date_d_iso_datetime() {
        let result = run_date(&["-d", "2024-06-15 14:30:00", "+%H:%M"]).await;
        assert_eq!(result.exit_code, 0);
        // In UTC mode this is exact; in local mode it depends on timezone
        assert!(result.stdout.trim().contains(':'));
    }

    #[tokio::test]
    async fn test_date_d_invalid() {
        let result = run_date(&["-d", "not a date"]).await;
        assert_eq!(result.exit_code, 1);
        assert!(result.stderr.contains("invalid date"));
    }

    #[tokio::test]
    async fn test_date_d_relative_weeks() {
        let result = run_date(&["-d", "2 weeks ago", "+%Y-%m-%d"]).await;
        assert_eq!(result.exit_code, 0);
        let date = result.stdout.trim();
        assert_eq!(date.len(), 10);
    }

    #[tokio::test]
    async fn test_date_d_plus_days() {
        let result = run_date(&["-d", "+7 days", "+%Y-%m-%d"]).await;
        assert_eq!(result.exit_code, 0);
        let date = result.stdout.trim();
        assert_eq!(date.len(), 10);
    }

    #[tokio::test]
    async fn test_date_long_date_flag() {
        let result = run_date(&["--date=yesterday", "+%Y-%m-%d"]).await;
        assert_eq!(result.exit_code, 0);
        let date = result.stdout.trim();
        assert_eq!(date.len(), 10);
    }

    // === Compound date expression tests ===

    #[tokio::test]
    async fn test_date_d_compound_date_minus_days() {
        // GNU date supports: date -d "2024-06-15 - 30 days"
        let result = run_date(&["-d", "2024-06-15 - 30 days", "+%Y-%m-%d"]).await;
        assert_eq!(result.exit_code, 0);
        assert_eq!(result.stdout.trim(), "2024-05-16");
    }

    #[tokio::test]
    async fn test_date_d_compound_date_plus_days() {
        // GNU date supports: date -d "2024-01-15 + 30 days"
        let result = run_date(&["-d", "2024-01-15 + 30 days", "+%Y-%m-%d"]).await;
        assert_eq!(result.exit_code, 0);
        assert_eq!(result.stdout.trim(), "2024-02-14");
    }

    #[tokio::test]
    async fn test_date_d_compound_date_minus_months() {
        let result = run_date(&["-d", "2024-03-15 - 2 months", "+%Y-%m-%d"]).await;
        assert_eq!(result.exit_code, 0);
        // 2 months ≈ 60 days, so 2024-03-15 - 60 days = 2024-01-15
        let date = result.stdout.trim();
        assert_eq!(date.len(), 10);
        assert!(date.starts_with("2024-01"));
    }

    #[tokio::test]
    async fn test_date_d_compound_epoch_minus_days() {
        // date -d "@1700000000 - 1 day"
        let result = run_date(&["-d", "@1700000000 - 1 day", "+%s"]).await;
        assert_eq!(result.exit_code, 0);
        let epoch: i64 = result.stdout.trim().parse().unwrap();
        assert_eq!(epoch, 1700000000 - 86400);
    }

    #[tokio::test]
    async fn test_date_d_compound_yesterday_plus_hours() {
        // date -d "yesterday + 12 hours"
        let result = run_date(&["-d", "yesterday + 12 hours", "+%Y-%m-%d"]).await;
        assert_eq!(result.exit_code, 0);
        let date = result.stdout.trim();
        assert_eq!(date.len(), 10);
    }

    // === --date= quote stripping tests ===

    #[tokio::test]
    async fn test_date_long_date_with_double_quotes() {
        // Parser bug: --date="30 days ago" passes literal quotes
        // The date builtin should strip them
        let result = run_date(&["--date=\"30 days ago\"", "+%Y-%m-%d"]).await;
        assert_eq!(result.exit_code, 0);
        let date = result.stdout.trim();
        assert_eq!(date.len(), 10);
    }

    #[tokio::test]
    async fn test_date_long_date_with_single_quotes() {
        let result = run_date(&["--date='yesterday'", "+%Y-%m-%d"]).await;
        assert_eq!(result.exit_code, 0);
        let date = result.stdout.trim();
        assert_eq!(date.len(), 10);
    }

    #[tokio::test]
    async fn test_date_lone_single_quote_input_no_panic() {
        let result = run_date(&["-d", "'", "+%Y-%m-%d"]).await;
        assert_eq!(result.exit_code, 1);
        assert!(result.stderr.contains("invalid date"));
    }

    #[tokio::test]
    async fn test_date_lone_double_quote_input_no_panic() {
        let result = run_date(&["-d", "\"", "+%Y-%m-%d"]).await;
        assert_eq!(result.exit_code, 1);
        assert!(result.stderr.contains("invalid date"));
    }

    // === --date with RFC 2822 input ===

    #[tokio::test]
    async fn test_date_parse_rfc2822_input() {
        let result = run_date(&["+%B %d, %Y", "--date=Mon, 06 Apr 2026 12:00:00 +0000"]).await;
        assert_eq!(result.exit_code, 0);
        assert_eq!(result.stdout.trim(), "April 06, 2026");
    }

    #[tokio::test]
    async fn test_date_parse_rfc2822_epoch_output() {
        let result = run_date(&["+%s", "--date=Wed, 01 Jan 2020 00:00:00 +0000"]).await;
        assert_eq!(result.exit_code, 0);
        assert_eq!(result.stdout.trim(), "1577836800");
    }

    #[tokio::test]
    async fn test_date_parse_rfc2822_mismatched_dow() {
        // April 11, 2026 is Saturday, not Thursday — GNU date ignores wrong DOW
        let result = run_date(&["+%Y-%m-%d", "--date=Thu, 11 Apr 2026 12:00:00 +0000"]).await;
        assert_eq!(result.exit_code, 0);
        assert_eq!(result.stdout.trim(), "2026-04-11");
    }

    #[tokio::test]
    async fn test_date_parse_rfc3339_input() {
        let result = run_date(&["+%Y", "--date=2024-06-15T12:00:00+00:00"]).await;
        assert_eq!(result.exit_code, 0);
        assert_eq!(result.stdout.trim(), "2024");
    }

    // === -R (RFC 2822) tests ===

    #[tokio::test]
    async fn test_date_rfc2822() {
        let result = run_date(&["-R"]).await;
        assert_eq!(result.exit_code, 0);
        let output = result.stdout.trim();
        // RFC 2822: "Mon, 15 Jan 2024 12:00:00 +0000"
        assert!(output.contains(','), "RFC 2822 should contain comma");
        assert!(output.len() > 20);
    }

    #[tokio::test]
    async fn test_date_rfc2822_utc() {
        let result = run_date(&["-u", "-R"]).await;
        assert_eq!(result.exit_code, 0);
        let output = result.stdout.trim();
        assert!(output.ends_with("+0000"));
    }

    // === -I (ISO 8601) tests ===

    #[tokio::test]
    async fn test_date_iso8601_default() {
        let result = run_date(&["-I"]).await;
        assert_eq!(result.exit_code, 0);
        let output = result.stdout.trim();
        // Just date: YYYY-MM-DD
        assert_eq!(output.len(), 10);
        assert!(output.contains('-'));
    }

    #[tokio::test]
    async fn test_date_iso8601_seconds() {
        let result = run_date(&["-Iseconds"]).await;
        assert_eq!(result.exit_code, 0);
        let output = result.stdout.trim();
        assert!(output.contains('T'));
        assert!(output.contains(':'));
    }

    // === %N (nanoseconds) tests ===

    #[tokio::test]
    async fn test_date_nanoseconds() {
        let result = run_date(&["+%N"]).await;
        assert_eq!(result.exit_code, 0);
        let output = result.stdout.trim();
        assert_eq!(output.len(), 9, "nanoseconds should be 9 digits");
        assert!(output.chars().all(|c| c.is_ascii_digit()));
    }

    #[tokio::test]
    async fn test_date_nanoseconds_in_format() {
        let result = run_date(&["+%S.%N"]).await;
        assert_eq!(result.exit_code, 0);
        let output = result.stdout.trim();
        assert!(output.contains('.'));
        let parts: Vec<&str> = output.split('.').collect();
        assert_eq!(parts.len(), 2);
        assert_eq!(parts[1].len(), 9);
    }

    #[test]
    fn test_expand_nanoseconds_basic() {
        assert_eq!(expand_nanoseconds("%N", 123456789), "123456789");
        assert_eq!(expand_nanoseconds("%N", 0), "000000000");
        assert_eq!(expand_nanoseconds("%S.%N", 42), "%S.000000042");
    }

    #[test]
    fn test_expand_nanoseconds_double_percent() {
        // %%N should become %N (literal %) after chrono processes %%
        // We only expand single %N, not %%N
        assert_eq!(expand_nanoseconds("%%N", 123), "%%N");
    }

    // Helper to run date with a pre-configured filesystem
    async fn run_date_with_fs(args: &[&str], fs: Arc<InMemoryFs>) -> ExecResult {
        let mut variables = HashMap::new();
        let env = HashMap::new();
        let mut cwd = PathBuf::from("/");

        let args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
        let ctx = Context {
            args: &args,
            env: &env,
            variables: &mut variables,
            cwd: &mut cwd,
            fs,
            stdin: None,
            #[cfg(feature = "http_client")]
            http_client: None,
            #[cfg(feature = "git")]
            git_client: None,
            #[cfg(feature = "ssh")]
            ssh_client: None,
            shell: None,
        };

        Date::new().execute(ctx).await.unwrap()
    }

    // === -r / --reference (file mtime) tests ===

    #[tokio::test]
    async fn test_date_r_file_mtime() {
        use crate::fs::FileSystem;

        let fs = Arc::new(InMemoryFs::new());
        fs.mkdir(std::path::Path::new("/tmp"), true).await.unwrap();
        fs.write_file(std::path::Path::new("/tmp/test.txt"), b"hello")
            .await
            .unwrap();

        // -r should return the file's mtime, not an error
        let result = run_date_with_fs(&["-r", "/tmp/test.txt", "+%Y-%m-%d"], fs).await;
        assert_eq!(result.exit_code, 0);
        let date = result.stdout.trim();
        // Should be a valid date (YYYY-MM-DD)
        assert_eq!(date.len(), 10);
        assert!(date.contains('-'));
    }

    #[tokio::test]
    async fn test_date_r_file_not_found() {
        let fs = Arc::new(InMemoryFs::new());
        let result = run_date_with_fs(&["-r", "/nonexistent.txt"], fs).await;
        assert_eq!(result.exit_code, 1);
        assert!(result.stderr.contains("cannot stat"));
        assert!(result.stderr.contains("/nonexistent.txt"));
    }

    #[tokio::test]
    async fn test_date_r_with_format() {
        use crate::fs::FileSystem;

        let fs = Arc::new(InMemoryFs::new());
        fs.mkdir(std::path::Path::new("/tmp"), true).await.unwrap();
        fs.write_file(std::path::Path::new("/tmp/test.txt"), b"content")
            .await
            .unwrap();

        let result = run_date_with_fs(&["-r", "/tmp/test.txt", "+%B"], fs).await;
        assert_eq!(result.exit_code, 0);
        // Should be a month name, non-empty
        let month = result.stdout.trim();
        assert!(!month.is_empty());
    }

    #[tokio::test]
    async fn test_date_reference_long_flag() {
        use crate::fs::FileSystem;

        let fs = Arc::new(InMemoryFs::new());
        fs.mkdir(std::path::Path::new("/tmp"), true).await.unwrap();
        fs.write_file(std::path::Path::new("/tmp/test.txt"), b"content")
            .await
            .unwrap();

        let result = run_date_with_fs(&["--reference=/tmp/test.txt", "+%Y"], fs).await;
        assert_eq!(result.exit_code, 0);
        let year = result.stdout.trim();
        assert_eq!(year.len(), 4);
    }

    #[tokio::test]
    async fn test_date_r_with_utc() {
        use crate::fs::FileSystem;

        let fs = Arc::new(InMemoryFs::new());
        fs.mkdir(std::path::Path::new("/tmp"), true).await.unwrap();
        fs.write_file(std::path::Path::new("/tmp/test.txt"), b"content")
            .await
            .unwrap();

        let result = run_date_with_fs(&["-u", "-r", "/tmp/test.txt", "+%Z"], fs).await;
        assert_eq!(result.exit_code, 0);
        let tz = result.stdout.trim();
        assert!(tz.contains("UTC") || tz == "+0000" || tz == "+00:00");
    }

    // TM-INF-022: invalid-date stderr must not leak `chrono` Debug shapes.
    #[tokio::test]
    async fn no_leak_invalid_format() {
        let r =
            crate::builtins::debug_leak_check::run(r#"date -d 'not a date in any format'"#).await;
        crate::builtins::debug_leak_check::assert_no_leak(
            &r,
            "date_invalid_format",
            &["chrono::ParseError", "ParseError {"],
        );
    }
}
