//! Condition evaluation for the rule-based strategy DSL.
//!
//! Converts a [`Condition`] tree (defined in `swym-dal`) into a `bool` by
//! inspecting the current [`SwymInstrumentData`] and pre-extracted position flags.

use barter::engine::state::instrument::data::InstrumentDataState;
use chrono::{DateTime, Utc};
use rust_decimal::{Decimal, MathematicalOps};
use rust_decimal_macros::dec;
use std::collections::{HashMap, VecDeque};
use swym_dal::models::strategy_config::{
    ArithOp, BollingerBand, CandleField, Comparison, CmpOp, Condition, CrossoverDirection,
    Expr, FuncName, PositionState, TrendDirection, UnaryOpKind,
};

/// Apply a [`CmpOp`] to two `usize` counts. Used by [`Condition::EventCount`].
fn apply_count_op(op: CmpOp, fired: usize, count: usize) -> bool {
    match op {
        CmpOp::Gt  => fired > count,
        CmpOp::Lt  => fired < count,
        CmpOp::Gte => fired >= count,
        CmpOp::Lte => fired <= count,
        CmpOp::Eq  => fired == count,
    }
}

use crate::strategy::{indicators, indicators::RunningEma, instrument_data::{CandleRecord, SwymInstrumentData}};

/// Evaluation context for a single instrument at candle-close time.
///
/// Position flags and position details are pre-extracted by the caller from the barter
/// `InstrumentState` so that `signal.rs` stays free of barter type dependencies.
pub struct EvalCtx<'a> {
    pub data: &'a SwymInstrumentData,
    pub is_flat: bool,
    pub is_long: bool,
    pub is_short: bool,
    /// The primary candle interval in seconds. Used to resolve expressions that
    /// omit the `timeframe` field (they default to the primary timeframe).
    pub primary_interval_secs: u64,
    /// Volume-weighted average entry price of the current position. `None` when flat.
    pub entry_price: Option<Decimal>,
    /// Absolute quantity of the current position in base asset units. `None` when flat.
    pub position_quantity: Option<Decimal>,
    /// Estimated unrealised PnL of the current position. `None` when flat.
    pub unrealised_pnl: Option<Decimal>,
    /// Time the current position was entered. `None` when flat.
    pub time_enter: Option<DateTime<Utc>>,
    /// Time of the current candle close (used for `BarsSinceEntry`). `None` when unavailable.
    pub current_time: Option<DateTime<Utc>>,
    /// Free balances by internal asset name (lowercased). `None` when unavailable.
    pub balances: Option<&'a HashMap<String, Decimal>>,
}

impl<'a> EvalCtx<'a> {
    /// Resolve an optional timeframe string to an interval in seconds.
    /// `None` → primary timeframe. Unknown string → 0 (history lookup returns `None`).
    fn resolve_interval(&self, timeframe: &Option<String>) -> u64 {
        match timeframe {
            None => self.primary_interval_secs,
            Some(s) => parse_interval_secs(s).unwrap_or(0),
        }
    }

    /// Return the candle history for the given timeframe (primary if `None`).
    fn resolve_candle_history(&self, timeframe: &Option<String>) -> Option<&VecDeque<CandleRecord>> {
        self.data.candle_history(self.resolve_interval(timeframe))
    }

    /// Return the trade-price history for the given timeframe (primary if `None`).
    fn resolve_trade_prices(&self, timeframe: &Option<String>) -> Option<&VecDeque<Decimal>> {
        self.data.trade_price_history(self.resolve_interval(timeframe))
    }

    /// Return the running EMA state for the given (timeframe, period) pair.
    fn resolve_ema_state(&self, timeframe: &Option<String>, period: usize) -> Option<&RunningEma> {
        self.data.ema_states.get(&(self.resolve_interval(timeframe), period))
    }
}

/// Recursively evaluate a [`Condition`] tree against the current instrument state.
pub fn evaluate(cond: &Condition, ctx: &EvalCtx<'_>) -> bool {
    match cond {
        Condition::AllOf { conditions } => conditions.iter().all(|sub| evaluate(sub, ctx)),
        Condition::AnyOf { conditions } => conditions.iter().any(|sub| evaluate(sub, ctx)),
        Condition::Not { condition } => !evaluate(condition, ctx),
        Condition::Position { state } => match state {
            PositionState::Flat => ctx.is_flat,
            PositionState::Long => ctx.is_long,
            PositionState::Short => ctx.is_short,
        },
        Condition::EmaCrossover { fast_period, slow_period, direction, timeframe } => {
            let Some(fast) = ctx.resolve_ema_state(timeframe, *fast_period) else { return false; };
            let Some(slow) = ctx.resolve_ema_state(timeframe, *slow_period) else { return false; };
            match direction {
                CrossoverDirection::Above => {
                    indicators::ema_crossed_above(fast, slow).unwrap_or(false)
                }
                CrossoverDirection::Below => {
                    indicators::ema_crossed_below(fast, slow).unwrap_or(false)
                }
            }
        }
        Condition::EmaTrend { period, direction, timeframe } => {
            let Some(ema) = ctx.resolve_ema_state(timeframe, *period) else { return false; };
            let Some(current_ema) = ema.current else { return false; };
            let Some(price) = ctx.data.price() else { return false; };
            match direction {
                TrendDirection::Above => price > current_ema,
                TrendDirection::Below => price < current_ema,
            }
        }
        Condition::Rsi { period, threshold, comparison, timeframe } => {
            let Some(prices) = ctx.resolve_trade_prices(timeframe) else { return false; };
            let Some(value) = indicators::rsi(prices, *period) else {
                return false;
            };
            match comparison {
                Comparison::Above => value > *threshold,
                Comparison::Below => value < *threshold,
            }
        }
        Condition::Bollinger { period, num_std_dev, band, timeframe } => {
            let Some(prices) = ctx.resolve_trade_prices(timeframe) else { return false; };
            let Some((_sma, upper, lower)) =
                indicators::bollinger_bands(prices, *period, *num_std_dev)
            else {
                return false;
            };
            let Some(price) = ctx.data.price() else { return false; };
            match band {
                BollingerBand::AboveUpper => price > upper,
                BollingerBand::BelowLower => price < lower,
            }
        }
        Condition::PriceLevel { price: level, direction, timeframe: _ } => {
            let Some(price) = ctx.data.price() else { return false; };
            match direction {
                TrendDirection::Above => price > *level,
                TrendDirection::Below => price < *level,
            }
        }
        Condition::Compare { left, op, right } => {
            let Some(l) = eval_expr(left, ctx) else { return false };
            let Some(r) = eval_expr(right, ctx) else { return false };
            match op {
                CmpOp::Gt  => l > r,
                CmpOp::Lt  => l < r,
                CmpOp::Gte => l >= r,
                CmpOp::Lte => l <= r,
                CmpOp::Eq  => l == r,
            }
        }
        Condition::CrossOver { left, right } => {
            let Some(l_now)  = eval_expr(left, ctx)              else { return false };
            let Some(r_now)  = eval_expr(right, ctx)             else { return false };
            let Some(l_prev) = eval_expr_at_offset(left, ctx, 1) else { return false };
            let Some(r_prev) = eval_expr_at_offset(right, ctx, 1) else { return false };
            l_prev <= r_prev && l_now > r_now
        }
        Condition::CrossUnder { left, right } => {
            let Some(l_now)  = eval_expr(left, ctx)              else { return false };
            let Some(r_now)  = eval_expr(right, ctx)             else { return false };
            let Some(l_prev) = eval_expr_at_offset(left, ctx, 1) else { return false };
            let Some(r_prev) = eval_expr_at_offset(right, ctx, 1) else { return false };
            l_prev >= r_prev && l_now < r_now
        }
        Condition::EventCount { condition, period, op, count } => {
            let history_len = ctx.resolve_candle_history(&None).map_or(0, |h| h.len());
            if history_len < *period {
                return false; // warm-up guard
            }
            let fired = (1..=*period)
                .filter(|&offset| evaluate_at_offset(condition, ctx, offset))
                .count();
            apply_count_op(*op, fired, *count)
        }
    }
}

/// Evaluate a [`Condition`] shifted `offset` bars into the past.
///
/// `offset = 0` delegates to [`evaluate`]. At `offset > 0`:
/// - `Compare`, `CrossOver`, `CrossUnder`, `AllOf`, `AnyOf`, `Not`, and `EventCount`
///   propagate the offset through to their `Expr` and sub-condition evaluations.
/// - `Position`, `EmaCrossover`, `EmaTrend`, `Rsi`, `Bollinger`, and `PriceLevel`
///   lack historical data at non-zero offsets and return `false`.
fn evaluate_at_offset(cond: &Condition, ctx: &EvalCtx<'_>, offset: usize) -> bool {
    if offset == 0 {
        return evaluate(cond, ctx);
    }
    match cond {
        Condition::AllOf { conditions } => {
            conditions.iter().all(|sub| evaluate_at_offset(sub, ctx, offset))
        }
        Condition::AnyOf { conditions } => {
            conditions.iter().any(|sub| evaluate_at_offset(sub, ctx, offset))
        }
        Condition::Not { condition } => !evaluate_at_offset(condition, ctx, offset),
        Condition::Compare { left, op, right } => {
            let Some(l) = eval_expr_at_offset(left, ctx, offset) else { return false };
            let Some(r) = eval_expr_at_offset(right, ctx, offset) else { return false };
            match op {
                CmpOp::Gt  => l > r,
                CmpOp::Lt  => l < r,
                CmpOp::Gte => l >= r,
                CmpOp::Lte => l <= r,
                CmpOp::Eq  => l == r,
            }
        }
        Condition::CrossOver { left, right } => {
            // At offset K, "current" = bar[-K] and "previous" = bar[-(K+1)].
            let Some(l_now)  = eval_expr_at_offset(left,  ctx, offset)     else { return false };
            let Some(r_now)  = eval_expr_at_offset(right, ctx, offset)     else { return false };
            let Some(l_prev) = eval_expr_at_offset(left,  ctx, offset + 1) else { return false };
            let Some(r_prev) = eval_expr_at_offset(right, ctx, offset + 1) else { return false };
            l_prev <= r_prev && l_now > r_now
        }
        Condition::CrossUnder { left, right } => {
            let Some(l_now)  = eval_expr_at_offset(left,  ctx, offset)     else { return false };
            let Some(r_now)  = eval_expr_at_offset(right, ctx, offset)     else { return false };
            let Some(l_prev) = eval_expr_at_offset(left,  ctx, offset + 1) else { return false };
            let Some(r_prev) = eval_expr_at_offset(right, ctx, offset + 1) else { return false };
            l_prev >= r_prev && l_now < r_now
        }
        Condition::EventCount { condition, period, op, count } => {
            // Nested event_count: scan relative to the current shifted position.
            let fired = (1..=*period)
                .filter(|&inner| evaluate_at_offset(condition, ctx, offset + inner))
                .count();
            apply_count_op(*op, fired, *count)
        }
        // No historical data available for these at non-zero offsets:
        // Position (no position history), EmaCrossover/EmaTrend (RunningEma stores
        // only current/prev), Rsi/Bollinger (would require re-computing from history),
        // PriceLevel (trivial but rarely useful in event_count context).
        _ => false,
    }
}

// --- Audit-aware evaluation ---

/// Evaluate a [`Condition`] tree without short-circuiting, recording every leaf
/// condition's result and intermediate values into `audits`.
///
/// Unlike [`evaluate`], `AllOf` and `AnyOf` evaluate **all** children regardless of
/// intermediate results so that the audit captures every condition's pass rate.
/// The aggregate boolean (AND / OR) is computed from the collected results.
pub fn evaluate_with_audit(
    cond: &Condition,
    ctx: &EvalCtx<'_>,
    path: &str,
    audits: &mut Vec<crate::strategy::audit::ConditionAudit>,
) -> bool {
    use crate::strategy::audit::ConditionAudit;
    match cond {
        Condition::AllOf { conditions } => {
            let results: Vec<bool> = conditions
                .iter()
                .enumerate()
                .map(|(i, sub)| {
                    evaluate_with_audit(sub, ctx, &format!("{path}.conditions[{i}]"), audits)
                })
                .collect();
            results.iter().all(|&r| r)
        }
        Condition::AnyOf { conditions } => {
            let results: Vec<bool> = conditions
                .iter()
                .enumerate()
                .map(|(i, sub)| {
                    evaluate_with_audit(sub, ctx, &format!("{path}.conditions[{i}]"), audits)
                })
                .collect();
            results.iter().any(|&r| r)
        }
        Condition::Not { condition } => {
            // Child records itself; we just negate.
            !evaluate_with_audit(condition, ctx, &format!("{path}.condition"), audits)
        }
        Condition::Compare { left, op, right } => {
            let l = eval_expr(left, ctx);
            let r = eval_expr(right, ctx);
            let insufficient = l.is_none() || r.is_none();
            let result = match (l, r) {
                (Some(lv), Some(rv)) => match op {
                    CmpOp::Gt  => lv > rv,
                    CmpOp::Lt  => lv < rv,
                    CmpOp::Gte => lv >= rv,
                    CmpOp::Lte => lv <= rv,
                    CmpOp::Eq  => lv == rv,
                },
                _ => false,
            };
            audits.push(ConditionAudit {
                path: path.to_string(),
                kind: "compare".to_string(),
                comment: None,
                result,
                insufficient_data: insufficient,
                left_value: l.map(|v| v.to_string()),
                right_value: r.map(|v| v.to_string()),
            });
            result
        }
        Condition::CrossOver { left, right } => {
            let l_now  = eval_expr(left, ctx);
            let r_now  = eval_expr(right, ctx);
            let l_prev = eval_expr_at_offset(left, ctx, 1);
            let r_prev = eval_expr_at_offset(right, ctx, 1);
            let insufficient = l_now.is_none() || r_now.is_none() || l_prev.is_none() || r_prev.is_none();
            let result = match (l_now, r_now, l_prev, r_prev) {
                (Some(ln), Some(rn), Some(lp), Some(rp)) => lp <= rp && ln > rn,
                _ => false,
            };
            audits.push(ConditionAudit {
                path: path.to_string(),
                kind: "cross_over".to_string(),
                comment: None,
                result,
                insufficient_data: insufficient,
                left_value: l_now.map(|v| v.to_string()),
                right_value: r_now.map(|v| v.to_string()),
            });
            result
        }
        Condition::CrossUnder { left, right } => {
            let l_now  = eval_expr(left, ctx);
            let r_now  = eval_expr(right, ctx);
            let l_prev = eval_expr_at_offset(left, ctx, 1);
            let r_prev = eval_expr_at_offset(right, ctx, 1);
            let insufficient = l_now.is_none() || r_now.is_none() || l_prev.is_none() || r_prev.is_none();
            let result = match (l_now, r_now, l_prev, r_prev) {
                (Some(ln), Some(rn), Some(lp), Some(rp)) => lp >= rp && ln < rn,
                _ => false,
            };
            audits.push(ConditionAudit {
                path: path.to_string(),
                kind: "cross_under".to_string(),
                comment: None,
                result,
                insufficient_data: insufficient,
                left_value: l_now.map(|v| v.to_string()),
                right_value: r_now.map(|v| v.to_string()),
            });
            result
        }
        Condition::Position { state } => {
            let result = match state {
                PositionState::Flat  => ctx.is_flat,
                PositionState::Long  => ctx.is_long,
                PositionState::Short => ctx.is_short,
            };
            audits.push(ConditionAudit {
                path: path.to_string(),
                kind: "position".to_string(),
                comment: None,
                result,
                insufficient_data: false,
                left_value: None,
                right_value: None,
            });
            result
        }
        Condition::EmaCrossover { fast_period, slow_period, direction, timeframe } => {
            let fast = ctx.resolve_ema_state(timeframe, *fast_period);
            let slow = ctx.resolve_ema_state(timeframe, *slow_period);
            let insufficient = fast.is_none() || slow.is_none()
                || fast.and_then(|e| e.current).is_none()
                || slow.and_then(|e| e.current).is_none();
            let result = if let (Some(f), Some(s)) = (fast, slow) {
                match direction {
                    CrossoverDirection::Above => indicators::ema_crossed_above(f, s).unwrap_or(false),
                    CrossoverDirection::Below => indicators::ema_crossed_below(f, s).unwrap_or(false),
                }
            } else {
                false
            };
            audits.push(ConditionAudit {
                path: path.to_string(),
                kind: "ema_crossover".to_string(),
                comment: None,
                result,
                insufficient_data: insufficient,
                left_value: fast.and_then(|e| e.current).map(|v| v.to_string()),
                right_value: slow.and_then(|e| e.current).map(|v| v.to_string()),
            });
            result
        }
        Condition::EmaTrend { period, direction, timeframe } => {
            let ema = ctx.resolve_ema_state(timeframe, *period);
            let ema_val = ema.and_then(|e| e.current);
            let price = ctx.data.price();
            let insufficient = ema_val.is_none() || price.is_none();
            let result = match (price, ema_val) {
                (Some(p), Some(e)) => match direction {
                    TrendDirection::Above => p > e,
                    TrendDirection::Below => p < e,
                },
                _ => false,
            };
            audits.push(ConditionAudit {
                path: path.to_string(),
                kind: "ema_trend".to_string(),
                comment: None,
                result,
                insufficient_data: insufficient,
                left_value: price.map(|v| v.to_string()),
                right_value: ema_val.map(|v| v.to_string()),
            });
            result
        }
        Condition::Rsi { period, threshold, comparison, timeframe } => {
            let prices = ctx.resolve_trade_prices(timeframe);
            let rsi_val = prices.and_then(|p| indicators::rsi(p, *period));
            let insufficient = rsi_val.is_none();
            let result = match rsi_val {
                Some(v) => match comparison {
                    Comparison::Above => v > *threshold,
                    Comparison::Below => v < *threshold,
                },
                None => false,
            };
            audits.push(ConditionAudit {
                path: path.to_string(),
                kind: "rsi".to_string(),
                comment: None,
                result,
                insufficient_data: insufficient,
                left_value: rsi_val.map(|v| v.to_string()),
                right_value: Some(threshold.to_string()),
            });
            result
        }
        Condition::Bollinger { period, num_std_dev, band, timeframe } => {
            let prices = ctx.resolve_trade_prices(timeframe);
            let bands = prices.and_then(|p| indicators::bollinger_bands(p, *period, *num_std_dev));
            let price = ctx.data.price();
            let insufficient = bands.is_none() || price.is_none();
            let result = match (&bands, price) {
                (Some((_sma, upper, lower)), Some(p)) => match band {
                    BollingerBand::AboveUpper => p > *upper,
                    BollingerBand::BelowLower => p < *lower,
                },
                _ => false,
            };
            let band_value = bands.map(|(_s, u, l)| match band {
                BollingerBand::AboveUpper => u.to_string(),
                BollingerBand::BelowLower => l.to_string(),
            });
            audits.push(ConditionAudit {
                path: path.to_string(),
                kind: "bollinger".to_string(),
                comment: None,
                result,
                insufficient_data: insufficient,
                left_value: price.map(|v| v.to_string()),
                right_value: band_value,
            });
            result
        }
        Condition::PriceLevel { price: level, direction, timeframe: _ } => {
            let price = ctx.data.price();
            let result = match price {
                Some(p) => match direction {
                    TrendDirection::Above => p > *level,
                    TrendDirection::Below => p < *level,
                },
                None => false,
            };
            audits.push(ConditionAudit {
                path: path.to_string(),
                kind: "price_level".to_string(),
                comment: None,
                result,
                insufficient_data: price.is_none(),
                left_value: price.map(|v| v.to_string()),
                right_value: Some(level.to_string()),
            });
            result
        }
        Condition::EventCount { condition, period, op, count } => {
            let history_len = ctx.resolve_candle_history(&None).map_or(0, |h| h.len());
            let insufficient = history_len < *period;
            let fired = if insufficient {
                0
            } else {
                (1..=*period)
                    .filter(|&offset| evaluate_at_offset(condition, ctx, offset))
                    .count()
            };
            let result = !insufficient && apply_count_op(*op, fired, *count);
            audits.push(ConditionAudit {
                path: path.to_string(),
                kind: "event_count".to_string(),
                comment: None,
                result,
                insufficient_data: insufficient,
                left_value: if insufficient { None } else { Some(fired.to_string()) },
                right_value: Some(count.to_string()),
            });
            result
        }
    }
}

// --- Expression evaluator ---

/// Evaluate a numeric [`Expr`] against the current candle history.
/// Returns `None` when there is insufficient history for the expression.
pub fn eval_expr(expr: &Expr, ctx: &EvalCtx<'_>) -> Option<Decimal> {
    eval_expr_at_offset(expr, ctx, 0)
}

/// Evaluate a numeric [`Expr`] with an additional bar offset applied to all
/// `Field`, `Func`, and `ApplyFunc` nodes. Used by `CrossOver`/`CrossUnder` to
/// compare expressions at the current bar vs. the previous bar.
fn eval_expr_at_offset(expr: &Expr, ctx: &EvalCtx<'_>, extra: usize) -> Option<Decimal> {
    match expr {
        Expr::Literal { value } => Some(*value),
        Expr::Field { field, offset, timeframe } => {
            let total = offset + extra;
            let history = ctx.resolve_candle_history(timeframe)?;
            let idx = history.len().checked_sub(1 + total)?;
            Some(get_candle_field(history.get(idx)?, *field))
        }
        Expr::Func { name, field, period, offset, multiplier, timeframe } => {
            let history = ctx.resolve_candle_history(timeframe)?;
            eval_func(*name, *field, *period, offset + extra, *multiplier, history)
        }
        Expr::BinOp { op, left, right } => {
            let l = eval_expr_at_offset(left, ctx, extra)?;
            let r = eval_expr_at_offset(right, ctx, extra)?;
            match op {
                ArithOp::Add => Some(l + r),
                ArithOp::Sub => Some(l - r),
                ArithOp::Mul => Some(l * r),
                ArithOp::Div => if r.is_zero() { None } else { Some(l / r) },
            }
        }
        Expr::ApplyFunc { name, input, period, offset, timeframe: _ } => {
            eval_apply_func(*name, input, *period, offset + extra, ctx)
        }
        Expr::UnaryOp { op, operand } => {
            let v = eval_expr_at_offset(operand, ctx, extra)?;
            match op {
                UnaryOpKind::Abs  => Some(v.abs()),
                UnaryOpKind::Sqrt => v.sqrt(),
                UnaryOpKind::Neg  => Some(-v),
                UnaryOpKind::Log  => Some(v.ln()),
            }
        }
        Expr::BarsSince { condition, period } => {
            // Scan back up to `period` bars relative to the current `extra` offset.
            // bar_offset 1 = previous bar, 2 = two bars ago, etc.
            (1..=*period)
                .find(|&bar_offset| evaluate_at_offset(condition, ctx, extra + bar_offset))
                .map(|bar_offset| Decimal::from(bar_offset as u64))
        }

        // Position-state expressions are only meaningful at the current bar (extra == 0).
        Expr::EntryPrice     => if extra == 0 { ctx.entry_price }     else { None },
        Expr::PositionQuantity => if extra == 0 { ctx.position_quantity } else { None },
        Expr::UnrealisedPnl  => if extra == 0 { ctx.unrealised_pnl }  else { None },

        Expr::BarsSinceEntry => {
            if extra != 0 { return None; }
            let enter = ctx.time_enter?;
            let now = ctx.current_time?;
            let elapsed_secs = (now - enter).num_seconds();
            let interval = ctx.primary_interval_secs as i64;
            if interval <= 0 { return None; }
            Some(Decimal::from(elapsed_secs / interval))
        }

        Expr::Balance { asset } => {
            if extra != 0 { return None; }
            ctx.balances?.get(asset.as_str()).copied()
        }
    }
}

fn get_candle_field(candle: &CandleRecord, field: CandleField) -> Decimal {
    match field {
        CandleField::Open   => candle.open,
        CandleField::High   => candle.high,
        CandleField::Low    => candle.low,
        CandleField::Close  => candle.close,
        CandleField::Volume => candle.volume,
    }
}

/// Evaluate a rolling-window function applied to a candle field.
///
/// `offset` = how many bars from the end to shift the window (0 = window ending on the most recent bar).
/// `multiplier` = optional multiplier parameter used by Supertrend (default 3.0).
fn eval_func(
    name: FuncName,
    field: CandleField,
    period: usize,
    offset: usize,
    multiplier: Option<Decimal>,
    history: &VecDeque<CandleRecord>,
) -> Option<Decimal> {

    match name {
        // --- Single-field rolling functions ---
        FuncName::Highest | FuncName::Lowest | FuncName::Sma | FuncName::Ema
        | FuncName::Rsi | FuncName::StdDev | FuncName::Wma | FuncName::Sum
        | FuncName::BollingerUpper | FuncName::BollingerLower => {
            if history.len() < offset + period {
                return None;
            }
            let end = history.len() - offset;
            let start = end - period;

            match name {
                FuncName::Highest => {
                    history.iter().skip(start).take(period)
                        .map(|c| get_candle_field(c, field))
                        .max()
                }
                FuncName::Lowest => {
                    history.iter().skip(start).take(period)
                        .map(|c| get_candle_field(c, field))
                        .min()
                }
                FuncName::Sma => {
                    let sum: Decimal = history.iter().skip(start).take(period)
                        .map(|c| get_candle_field(c, field))
                        .sum();
                    Some(sum / Decimal::from(period as u64))
                }
                FuncName::Ema => {
                    let k = Decimal::TWO / Decimal::from((period + 1) as u64);
                    let mut ema: Option<Decimal> = None;
                    for candle in history.iter().skip(start).take(period) {
                        let price = get_candle_field(candle, field);
                        ema = Some(match ema {
                            None       => price,
                            Some(prev) => (price - prev) * k + prev,
                        });
                    }
                    ema
                }
                FuncName::Rsi => {
                    let prices: VecDeque<Decimal> = history.iter().skip(start).take(period)
                        .map(|c| get_candle_field(c, field))
                        .collect();
                    indicators::rsi(&prices, period.saturating_sub(1).max(1))
                }
                FuncName::StdDev => {
                    let values: Vec<Decimal> = history.iter().skip(start).take(period)
                        .map(|c| get_candle_field(c, field))
                        .collect();
                    let count = Decimal::from(period as u64);
                    let mean: Decimal = values.iter().sum::<Decimal>() / count;
                    let variance: Decimal = values.iter()
                        .map(|&v| { let d = v - mean; d * d })
                        .sum::<Decimal>() / count;
                    variance.sqrt()
                }
                FuncName::Wma => {
                    // Weight of bar i (0=oldest, period-1=newest) = i + 1
                    let weight_sum = Decimal::from((period * (period + 1) / 2) as u64);
                    let weighted: Decimal = history.iter().skip(start).take(period)
                        .enumerate()
                        .map(|(i, c)| get_candle_field(c, field) * Decimal::from((i + 1) as u64))
                        .sum();
                    Some(weighted / weight_sum)
                }
                FuncName::Sum => {
                    Some(history.iter().skip(start).take(period)
                        .map(|c| get_candle_field(c, field))
                        .sum())
                }
                FuncName::BollingerUpper | FuncName::BollingerLower => {
                    let values: Vec<Decimal> = history.iter().skip(start).take(period)
                        .map(|c| get_candle_field(c, field))
                        .collect();
                    let count = Decimal::from(period as u64);
                    let mean: Decimal = values.iter().sum::<Decimal>() / count;
                    let variance: Decimal = values.iter()
                        .map(|&v| { let d = v - mean; d * d })
                        .sum::<Decimal>() / count;
                    let std_dev = variance.sqrt()?;
                    let num_std = multiplier.unwrap_or(Decimal::TWO);
                    if matches!(name, FuncName::BollingerUpper) {
                        Some(mean + num_std * std_dev)
                    } else {
                        Some(mean - num_std * std_dev)
                    }
                }
                _ => unreachable!(),
            }
        }

        // --- Multi-field functions (use High, Low, Close internally; `field` ignored) ---

        FuncName::Atr => {
            // Simple ATR: average True Range over `period` bars.
            // Needs period+1 candles (each TR requires a previous close).
            if history.len() < offset + period + 1 {
                return None;
            }
            let end = history.len() - offset;
            let start = end - period; // TR from start..end, each needs start-1..end-1 as prev

            let tr_sum: Decimal = (start..end).filter_map(|i| {
                let c = history.get(i)?;
                let p = history.get(i.checked_sub(1)?)?;
                let hl  = c.high - c.low;
                let hpc = (c.high - p.close).abs();
                let lpc = (c.low  - p.close).abs();
                Some(hl.max(hpc).max(lpc))
            }).sum();

            Some(tr_sum / Decimal::from(period as u64))
        }

        FuncName::Supertrend => {
            // Stateful Supertrend: iterates over history to compute the band-flip line.
            // period = ATR period, multiplier = ATR multiplier (default 3.0).
            // Returns the current supertrend value (below price = bullish, above = bearish).
            let mult = multiplier.unwrap_or(dec!(3.0));
            let end = history.len().checked_sub(offset)?;

            if end <= period {
                return None;
            }

            let period_dec = Decimal::from(period as u64);
            let mut prev_final_upper = Decimal::ZERO;
            let mut prev_final_lower = Decimal::ZERO;
            let mut prev_supertrend = Decimal::ZERO;
            let mut initialized = false;

            for i in period..end {
                // Simple ATR over [i-period+1 ..= i] (period TR values, each needs prev bar)
                let atr_sum: Decimal = ((i - period + 1)..=i).filter_map(|j| {
                    if j == 0 { return None; }
                    let c = history.get(j)?;
                    let p = history.get(j - 1)?;
                    let hl  = c.high - c.low;
                    let hpc = (c.high - p.close).abs();
                    let lpc = (c.low  - p.close).abs();
                    Some(hl.max(hpc).max(lpc))
                }).sum();
                let atr = atr_sum / period_dec;

                let curr = &history[i];
                let hl2 = (curr.high + curr.low) / Decimal::TWO;
                let basic_upper = hl2 + mult * atr;
                let basic_lower = hl2 - mult * atr;

                if !initialized {
                    prev_final_upper = basic_upper;
                    prev_final_lower = basic_lower;
                    prev_supertrend  = basic_lower; // start bullish
                    initialized = true;
                    continue;
                }

                let prev_close = history[i - 1].close;

                // Ratchet bands: only widen (lower can only move up, upper can only move down)
                // unless the previous close broke through them.
                let final_lower = if basic_lower > prev_final_lower || prev_close < prev_final_lower {
                    basic_lower
                } else {
                    prev_final_lower
                };
                let final_upper = if basic_upper < prev_final_upper || prev_close > prev_final_upper {
                    basic_upper
                } else {
                    prev_final_upper
                };

                let supertrend = if prev_supertrend == prev_final_upper {
                    // Was bearish
                    if curr.close > final_upper { final_lower } else { final_upper }
                } else {
                    // Was bullish
                    if curr.close < final_lower { final_upper } else { final_lower }
                };

                prev_final_upper = final_upper;
                prev_final_lower = final_lower;
                prev_supertrend  = supertrend;
            }

            if initialized { Some(prev_supertrend) } else { None }
        }

        FuncName::Adx => {
            // Average Directional Index (Wilder's smoothing).
            // Needs 2*period + 1 candles: period to seed TR/DM sums, period more to smooth ADX.
            let needed = 2 * period + 1;
            if history.len() < offset + needed {
                return None;
            }
            let end = history.len() - offset;
            let win_start = end - needed;
            let period_dec = Decimal::from(period as u64);

            // Compute TR, +DM, -DM for 2*period bars (each needs its previous bar).
            let mut trs: Vec<Decimal>      = Vec::with_capacity(2 * period);
            let mut plus_dms: Vec<Decimal> = Vec::with_capacity(2 * period);
            let mut minus_dms: Vec<Decimal> = Vec::with_capacity(2 * period);

            for i in (win_start + 1)..end {
                let c = &history[i];
                let p = &history[i - 1];
                let tr = (c.high - c.low)
                    .max((c.high - p.close).abs())
                    .max((c.low  - p.close).abs());
                let up = c.high - p.high;
                let dn = p.low  - c.low;
                trs.push(tr);
                plus_dms.push( if up > dn && up > Decimal::ZERO { up } else { Decimal::ZERO });
                minus_dms.push(if dn > up && dn > Decimal::ZERO { dn } else { Decimal::ZERO });
            }

            // Seed Wilder's smoothed TR/+DM/-DM from first `period` bars.
            let mut atr_w: Decimal = trs[..period].iter().sum();
            let mut pdm_w: Decimal = plus_dms[..period].iter().sum();
            let mut mdm_w: Decimal = minus_dms[..period].iter().sum();

            let dx_of = |atr: Decimal, pdm: Decimal, mdm: Decimal| -> Decimal {
                if atr.is_zero() { return Decimal::ZERO; }
                let pdi = dec!(100) * pdm / atr;
                let mdi = dec!(100) * mdm / atr;
                let s = pdi + mdi;
                if s.is_zero() { Decimal::ZERO } else { dec!(100) * (pdi - mdi).abs() / s }
            };

            // Seed ADX with the first DX value, then Wilder-smooth period times.
            let mut adx = dx_of(atr_w, pdm_w, mdm_w);
            for i in period..(2 * period) {
                atr_w = atr_w - atr_w / period_dec + trs[i];
                pdm_w = pdm_w - pdm_w / period_dec + plus_dms[i];
                mdm_w = mdm_w - mdm_w / period_dec + minus_dms[i];
                let dx = dx_of(atr_w, pdm_w, mdm_w);
                adx = (adx * (period_dec - Decimal::ONE) + dx) / period_dec;
            }

            Some(adx)
        }
    }
}

/// Evaluate a rolling function applied to an arbitrary sub-expression.
///
/// For each position in the `period`-bar window (oldest first), evaluates `input`
/// at the corresponding bar offset, collects the resulting series, then applies the
/// rolling function to the collected scalar values.
///
/// This enables function composition: EMA of EMA (Gaussian Channel), WMA of arithmetic
/// (Hull MA), VWAP via `Sum(close × volume) / Sum(volume)`, etc.
///
/// Not all [`FuncName`] variants are valid — ATR, Supertrend, ADX, and RSI require
/// per-bar multi-field data and are not compatible with a pre-evaluated scalar series;
/// they return `None`.
fn eval_apply_func(
    name: FuncName,
    input: &Expr,
    period: usize,
    offset: usize,
    ctx: &EvalCtx<'_>,
) -> Option<Decimal> {
    if period == 0 {
        return None;
    }

    // Collect `period` values oldest-first.
    // For position i (0=oldest, period-1=newest), the bar is at extra_offset = offset + (period-1-i).
    let mut values: Vec<Decimal> = Vec::with_capacity(period);
    for i in 0..period {
        let bar_offset = offset + (period - 1 - i);
        let v = eval_expr_at_offset(input, ctx, bar_offset)?;
        values.push(v);
    }

    match name {
        FuncName::Sma => {
            let sum: Decimal = values.iter().sum();
            Some(sum / Decimal::from(period as u64))
        }
        FuncName::Ema => {
            let k = Decimal::TWO / Decimal::from((period + 1) as u64);
            let mut ema: Option<Decimal> = None;
            for &v in &values {
                ema = Some(match ema {
                    None       => v,
                    Some(prev) => (v - prev) * k + prev,
                });
            }
            ema
        }
        FuncName::Wma => {
            let weight_sum = Decimal::from((period * (period + 1) / 2) as u64);
            let weighted: Decimal = values.iter().enumerate()
                .map(|(i, &v)| v * Decimal::from((i + 1) as u64))
                .sum();
            Some(weighted / weight_sum)
        }
        FuncName::Sum => Some(values.iter().sum()),
        FuncName::Highest => values.into_iter().max(),
        FuncName::Lowest  => values.into_iter().min(),
        FuncName::StdDev  => {
            let count = Decimal::from(period as u64);
            let mean: Decimal = values.iter().sum::<Decimal>() / count;
            let variance: Decimal = values.iter()
                .map(|&v| { let d = v - mean; d * d })
                .sum::<Decimal>() / count;
            variance.sqrt()
        }
        FuncName::BollingerUpper | FuncName::BollingerLower => {
            let count = Decimal::from(period as u64);
            let mean: Decimal = values.iter().sum::<Decimal>() / count;
            let variance: Decimal = values.iter()
                .map(|&v| { let d = v - mean; d * d })
                .sum::<Decimal>() / count;
            let std_dev = variance.sqrt()?;
            if matches!(name, FuncName::BollingerUpper) {
                Some(mean + Decimal::TWO * std_dev)
            } else {
                Some(mean - Decimal::TWO * std_dev)
            }
        }
        // Multi-field or stateful functions: not compatible with scalar series input.
        FuncName::Rsi | FuncName::Atr | FuncName::Supertrend | FuncName::Adx => None,
    }
}


/// Parse a candle interval string (e.g. `"1m"`, `"5m"`) into seconds.
///
/// Returns `None` for unrecognised strings.
pub fn parse_interval_secs(interval: &str) -> Option<u64> {
    match interval {
        "1m" => Some(60),
        "5m" => Some(300),
        "15m" => Some(900),
        "1h" => Some(3_600),
        "4h" => Some(14_400),
        "1d" => Some(86_400),
        _ => None,
    }
}

/// Convert an interval in seconds back to the canonical string form used by the DB.
///
/// Returns `None` for unrecognised values.
pub fn interval_secs_to_str(secs: u64) -> Option<&'static str> {
    match secs {
        60     => Some("1m"),
        300    => Some("5m"),
        900    => Some("15m"),
        3_600  => Some("1h"),
        14_400 => Some("4h"),
        86_400 => Some("1d"),
        _ => None,
    }
}

/// Collect all (interval_secs, period) EMA registration pairs referenced in a condition tree.
///
/// Used to pre-register [`RunningEma`] instances keyed by timeframe before the first market event.
/// `primary_secs` is substituted for expressions that omit the `timeframe` field.
pub fn collect_ema_registrations(cond: &Condition, primary_secs: u64, out: &mut Vec<(u64, usize)>) {
    match cond {
        Condition::AllOf { conditions } => {
            conditions.iter().for_each(|sub| collect_ema_registrations(sub, primary_secs, out));
        }
        Condition::AnyOf { conditions } => {
            conditions.iter().for_each(|sub| collect_ema_registrations(sub, primary_secs, out));
        }
        Condition::Not { condition } => collect_ema_registrations(condition, primary_secs, out),
        Condition::EmaCrossover { fast_period, slow_period, timeframe, .. } => {
            let secs = timeframe.as_ref()
                .and_then(|tf| parse_interval_secs(tf))
                .unwrap_or(primary_secs);
            out.push((secs, *fast_period));
            out.push((secs, *slow_period));
        }
        Condition::EmaTrend { period, timeframe, .. } => {
            let secs = timeframe.as_ref()
                .and_then(|tf| parse_interval_secs(tf))
                .unwrap_or(primary_secs);
            out.push((secs, *period));
        }
        Condition::EventCount { condition, .. } => {
            collect_ema_registrations(condition, primary_secs, out);
        }
        _ => {}
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use rust_decimal_macros::dec;
    use swym_dal::models::strategy_config::{Condition, PositionState};
    use crate::strategy::instrument_data::CandleRecord;

    const TEST_INTERVAL: u64 = 300; // 5m sentinel used by test helpers

    fn flat_ctx(data: &SwymInstrumentData) -> EvalCtx<'_> {
        EvalCtx {
            data,
            is_flat: true, is_long: false, is_short: false,
            primary_interval_secs: TEST_INTERVAL,
            entry_price: None, position_quantity: None, unrealised_pnl: None,
            time_enter: None, current_time: None, balances: None,
        }
    }

    fn long_ctx(data: &SwymInstrumentData) -> EvalCtx<'_> {
        EvalCtx {
            data,
            is_flat: false, is_long: true, is_short: false,
            primary_interval_secs: TEST_INTERVAL,
            entry_price: None, position_quantity: None, unrealised_pnl: None,
            time_enter: None, current_time: None, balances: None,
        }
    }

    fn make_candle(close: f64) -> CandleRecord {
        let c = Decimal::from_f64_retain(close).unwrap();
        CandleRecord { open: c, high: c, low: c, close: c, volume: dec!(100), open_time: None }
    }

    fn make_candle_ohlcv(open: f64, high: f64, low: f64, close: f64, volume: f64) -> CandleRecord {
        CandleRecord {
            open:   Decimal::from_f64_retain(open).unwrap(),
            high:   Decimal::from_f64_retain(high).unwrap(),
            low:    Decimal::from_f64_retain(low).unwrap(),
            close:  Decimal::from_f64_retain(close).unwrap(),
            volume: Decimal::from_f64_retain(volume).unwrap(),
            open_time: None,
        }
    }

    #[test]
    fn position_flat_matches() {
        let data = SwymInstrumentData::default();
        let cond = Condition::Position { state: PositionState::Flat };
        assert!(evaluate(&cond, &flat_ctx(&data)));
        assert!(!evaluate(&cond, &long_ctx(&data)));
    }

    #[test]
    fn all_of_short_circuits_on_false() {
        let data = SwymInstrumentData::default();
        let cond = Condition::AllOf {
            conditions: vec![
                Condition::Position { state: PositionState::Long },
                Condition::Position { state: PositionState::Short },
            ],
        };
        assert!(!evaluate(&cond, &flat_ctx(&data)));
    }

    #[test]
    fn any_of_returns_true_if_one_matches() {
        let data = SwymInstrumentData::default();
        let cond = Condition::AnyOf {
            conditions: vec![
                Condition::Position { state: PositionState::Long },
                Condition::Position { state: PositionState::Flat },
            ],
        };
        assert!(evaluate(&cond, &flat_ctx(&data)));
    }

    #[test]
    fn not_negates() {
        let data = SwymInstrumentData::default();
        let cond = Condition::Not {
            condition: Box::new(Condition::Position { state: PositionState::Flat }),
        };
        assert!(!evaluate(&cond, &flat_ctx(&data)));
        assert!(evaluate(&cond, &long_ctx(&data)));
    }

    #[test]
    fn parse_interval_secs_known_values() {
        assert_eq!(parse_interval_secs("1m"), Some(60));
        assert_eq!(parse_interval_secs("5m"), Some(300));
        assert_eq!(parse_interval_secs("1h"), Some(3_600));
        assert_eq!(parse_interval_secs("1d"), Some(86_400));
        assert_eq!(parse_interval_secs("2m"), None);
    }


    #[test]
    fn price_level_above_returns_false_when_no_price() {
        let data = SwymInstrumentData::default();
        let cond = Condition::PriceLevel {
            price: dec!(50000),
            direction: TrendDirection::Above,
            timeframe: None,
        };
        assert!(!evaluate(&cond, &flat_ctx(&data)));
    }

    // --- Expression evaluator tests ---

    fn data_with_candles(candles: Vec<CandleRecord>) -> SwymInstrumentData {
        let mut data = SwymInstrumentData::default();
        data.primary_interval_secs = Some(TEST_INTERVAL);
        data.candle_histories.insert(TEST_INTERVAL, candles.into_iter().collect());
        data
    }

    #[test]
    fn eval_wma_uniform_prices() {
        // WMA of [10, 10, 10] with weights [1,2,3] = 10
        let candles: Vec<CandleRecord> = (0..3).map(|_| make_candle(10.0)).collect();
        let data = data_with_candles(candles);
        let ctx = flat_ctx(&data);
        let expr = Expr::Func {
            name: FuncName::Wma,
            field: CandleField::Close,
            period: 3,
            offset: 0,
            multiplier: None,
            timeframe: None,
        };
        let result = eval_expr(&expr, &ctx).unwrap();
        assert_eq!(result, dec!(10));
    }

    #[test]
    fn eval_wma_increasing_prices() {
        // WMA of [1, 2, 3] with weights [1,2,3], weight_sum=6:
        // (1*1 + 2*2 + 3*3) / 6 = 14/6 ≈ 2.333...
        let candles: Vec<CandleRecord> = (1..=3).map(|i| make_candle(i as f64)).collect();
        let data = data_with_candles(candles);
        let ctx = flat_ctx(&data);
        let expr = Expr::Func {
            name: FuncName::Wma,
            field: CandleField::Close,
            period: 3,
            offset: 0,
            multiplier: None,
            timeframe: None,
        };
        let result = eval_expr(&expr, &ctx).unwrap();
        // (1 + 4 + 9) / 6 = 14/6
        assert_eq!(result, Decimal::from(14) / Decimal::from(6));
    }

    #[test]
    fn eval_sum_basic() {
        let candles: Vec<CandleRecord> = (1..=5).map(|i| make_candle(i as f64)).collect();
        let data = data_with_candles(candles);
        let ctx = flat_ctx(&data);
        let expr = Expr::Func {
            name: FuncName::Sum,
            field: CandleField::Close,
            period: 5,
            offset: 0,
            multiplier: None,
            timeframe: None,
        };
        let result = eval_expr(&expr, &ctx).unwrap();
        assert_eq!(result, dec!(15)); // 1+2+3+4+5
    }

    #[test]
    fn eval_atr_flat_market() {
        // Flat market: all OHLCV same → TR = 0, ATR = 0
        let candles: Vec<CandleRecord> = (0..15).map(|_| make_candle_ohlcv(100.0, 100.0, 100.0, 100.0, 1.0)).collect();
        let data = data_with_candles(candles);
        let ctx = flat_ctx(&data);
        let expr = Expr::Func {
            name: FuncName::Atr,
            field: CandleField::Close, // ignored for ATR
            period: 14,
            offset: 0,
            multiplier: None,
            timeframe: None,
        };
        let result = eval_expr(&expr, &ctx).unwrap();
        assert_eq!(result, dec!(0));
    }

    #[test]
    fn eval_atr_simple_range() {
        // Each candle has H-L = 2, no gap from prev close → TR = 2 each bar, ATR = 2
        let candles: Vec<CandleRecord> = (0..15).map(|i| {
            let mid = 100.0 + i as f64;
            make_candle_ohlcv(mid, mid + 1.0, mid - 1.0, mid, 1.0)
        }).collect();
        let data = data_with_candles(candles);
        let ctx = flat_ctx(&data);
        let expr = Expr::Func {
            name: FuncName::Atr,
            field: CandleField::Close,
            period: 14,
            offset: 0,
            multiplier: None,
            timeframe: None,
        };
        let result = eval_expr(&expr, &ctx).unwrap();
        assert_eq!(result, dec!(2));
    }

    #[test]
    fn eval_apply_func_ema_of_sma() {
        // EMA(3) applied to a series of SMA(3) values — just verify it returns Some
        let candles: Vec<CandleRecord> = (1..=20).map(|i| make_candle(i as f64)).collect();
        let data = data_with_candles(candles);
        let ctx = flat_ctx(&data);
        let inner = Expr::Func {
            name: FuncName::Sma,
            field: CandleField::Close,
            period: 3,
            offset: 0,
            multiplier: None,
            timeframe: None,
        };
        let expr = Expr::ApplyFunc {
            name: FuncName::Ema,
            input: Box::new(inner),
            period: 3,
            offset: 0,
            timeframe: None,
        };
        let result = eval_expr(&expr, &ctx);
        assert!(result.is_some(), "expected Some for EMA of SMA with sufficient history");
    }

    #[test]
    fn eval_apply_func_returns_none_for_atr() {
        // ATR is not valid inside ApplyFunc → None
        let candles: Vec<CandleRecord> = (1..=20).map(|i| make_candle(i as f64)).collect();
        let data = data_with_candles(candles);
        let ctx = flat_ctx(&data);
        let inner = Expr::Func {
            name: FuncName::Sma,
            field: CandleField::Close,
            period: 3,
            offset: 0,
            multiplier: None,
            timeframe: None,
        };
        let expr = Expr::ApplyFunc {
            name: FuncName::Atr, // not valid inside ApplyFunc
            input: Box::new(inner),
            period: 3,
            offset: 0,
            timeframe: None,
        };
        assert!(eval_expr(&expr, &ctx).is_none());
    }

    #[test]
    fn eval_unary_abs() {
        let data = SwymInstrumentData::default();
        let ctx = flat_ctx(&data);
        let expr = Expr::UnaryOp {
            op: UnaryOpKind::Abs,
            operand: Box::new(Expr::Literal { value: dec!(-5) }),
        };
        assert_eq!(eval_expr(&expr, &ctx), Some(dec!(5)));
    }

    #[test]
    fn eval_unary_neg() {
        let data = SwymInstrumentData::default();
        let ctx = flat_ctx(&data);
        let expr = Expr::UnaryOp {
            op: UnaryOpKind::Neg,
            operand: Box::new(Expr::Literal { value: dec!(3) }),
        };
        assert_eq!(eval_expr(&expr, &ctx), Some(dec!(-3)));
    }

    #[test]
    fn eval_supertrend_returns_some_with_enough_history() {
        // Just verify Supertrend doesn't panic and returns Some with enough candles
        let candles: Vec<CandleRecord> = (0..30).map(|i| {
            let p = 100.0 + i as f64 * 0.5;
            make_candle_ohlcv(p, p + 1.0, p - 1.0, p, 10.0)
        }).collect();
        let data = data_with_candles(candles);
        let ctx = flat_ctx(&data);
        let expr = Expr::Func {
            name: FuncName::Supertrend,
            field: CandleField::Close,
            period: 10,
            offset: 0,
            multiplier: Some(dec!(3.0)),
            timeframe: None,
        };
        assert!(eval_expr(&expr, &ctx).is_some());
    }

    #[test]
    fn eval_adx_returns_some_with_enough_history() {
        let candles: Vec<CandleRecord> = (0..35).map(|i| {
            let p = 100.0 + (i as f64).sin() * 5.0;
            make_candle_ohlcv(p, p + 1.5, p - 1.5, p, 10.0)
        }).collect();
        let data = data_with_candles(candles);
        let ctx = flat_ctx(&data);
        let expr = Expr::Func {
            name: FuncName::Adx,
            field: CandleField::Close,
            period: 14,
            offset: 0,
            multiplier: None,
            timeframe: None,
        };
        let result = eval_expr(&expr, &ctx);
        assert!(result.is_some());
        let adx = result.unwrap();
        assert!(adx >= dec!(0) && adx <= dec!(100), "ADX out of range: {adx}");
    }

    // --- EventCount ---

    #[test]
    fn event_count_returns_false_during_warmup() {
        let candles: Vec<CandleRecord> = (0..5).map(|_| make_candle(100.0)).collect();
        let data = data_with_candles(candles);
        let ctx = flat_ctx(&data);
        // period=10 but only 5 candles — warm-up guard must fire
        let cond = Condition::EventCount {
            condition: Box::new(Condition::Position { state: PositionState::Flat }),
            period: 10,
            op: CmpOp::Gte,
            count: 1,
        };
        assert!(!evaluate(&cond, &ctx));
    }

    #[test]
    fn event_count_cross_under_detects_past_event() {
        // Build a falling price that dips below a 3-bar SMA then rises back.
        // Bar layout (oldest → newest):
        //   [100, 100, 100, 100, 50, 100, 100, 100]
        // At the current bar (offset=0) close=100 > sma3.
        // At offset=4 (bar with close=50), close < sma3 → cross_under fired there.
        let prices = [100.0, 100.0, 100.0, 100.0, 50.0, 100.0, 100.0, 100.0];
        let candles: Vec<CandleRecord> = prices.iter().map(|&p| make_candle(p)).collect();
        let data = data_with_candles(candles);
        let ctx = flat_ctx(&data);

        let sma3 = Expr::Func {
            name: FuncName::Sma,
            field: CandleField::Close,
            period: 3,
            offset: 0,
            multiplier: None,
            timeframe: None,
        };
        let cross_under = Condition::CrossUnder {
            left: Expr::Field { field: CandleField::Close, offset: 0, timeframe: None },
            right: sma3,
        };

        // At least one cross_under in last 6 bars?
        let cond = Condition::EventCount {
            condition: Box::new(cross_under),
            period: 6,
            op: CmpOp::Gte,
            count: 1,
        };
        assert!(evaluate(&cond, &ctx), "expected at least one cross_under in history");
    }

    #[test]
    fn event_count_zero_means_never_fired() {
        // Monotonically increasing prices — close never crosses below SMA.
        let candles: Vec<CandleRecord> = (1..=20).map(|i| make_candle(i as f64)).collect();
        let data = data_with_candles(candles);
        let ctx = flat_ctx(&data);

        let sma5 = Expr::Func {
            name: FuncName::Sma,
            field: CandleField::Close,
            period: 5,
            offset: 0,
            multiplier: None,
            timeframe: None,
        };
        let cross_under = Condition::CrossUnder {
            left: Expr::Field { field: CandleField::Close, offset: 0, timeframe: None },
            right: sma5,
        };
        // Should have fired zero times (price always rising, never crosses under SMA).
        let cond = Condition::EventCount {
            condition: Box::new(cross_under),
            period: 10,
            op: CmpOp::Eq,
            count: 0,
        };
        assert!(evaluate(&cond, &ctx), "expected zero cross_unders in monotonically rising series");
    }

    // --- BarsSince ---

    #[test]
    fn bars_since_finds_correct_distance() {
        // Same price series as event_count test: dip at offset 4 from end.
        let prices = [100.0, 100.0, 100.0, 100.0, 50.0, 100.0, 100.0, 100.0];
        let candles: Vec<CandleRecord> = prices.iter().map(|&p| make_candle(p)).collect();
        let data = data_with_candles(candles);
        let ctx = flat_ctx(&data);

        // "close < 60" was true exactly at the dip bar (offset=4 from end)
        let below_60 = Condition::Compare {
            left: Expr::Field { field: CandleField::Close, offset: 0, timeframe: None },
            op: CmpOp::Lt,
            right: Expr::Literal { value: dec!(60) },
        };
        let expr = Expr::BarsSince {
            condition: Box::new(below_60),
            period: 10,
        };
        let result = eval_expr(&expr, &ctx);
        // Prices: [100,100,100,100,50,100,100,100] → dip at index 4, current is index 7 → 3 bars ago.
        assert_eq!(result, Some(dec!(3)), "dip was 3 bars ago (index 4 from end of 8-bar series)");
    }

    #[test]
    fn bars_since_returns_none_when_condition_never_fired() {
        let candles: Vec<CandleRecord> = (0..10).map(|_| make_candle(100.0)).collect();
        let data = data_with_candles(candles);
        let ctx = flat_ctx(&data);

        // "close < 50" never true
        let below_50 = Condition::Compare {
            left: Expr::Field { field: CandleField::Close, offset: 0, timeframe: None },
            op: CmpOp::Lt,
            right: Expr::Literal { value: dec!(50) },
        };
        let expr = Expr::BarsSince {
            condition: Box::new(below_50),
            period: 10,
        };
        assert_eq!(eval_expr(&expr, &ctx), None);
    }
}
