use std::collections::HashMap;
use std::sync::Arc;

use arrow::array::{
    ArrayRef, BooleanArray, Float64Array, TimestampNanosecondArray, UInt32Array, UInt8Array,
};
use arrow::record_batch::RecordBatch;
use bt_core::{
    BacktestConfig, BacktestRunner, BarInterval, ExecutionConfig, ExecutionPrice, FeeConfig,
    FillModel, OrderConfig, PositionSizingMode, SimpleBacktestRunner, SlippageConfig,
    StrategyMetadata, StrategyPlan,
};
use bt_data::{DataStore, DatasetVersion, WriteOptions};
use bt_expr::{compile_expr, Expr, ScalarValue};
use bt_kernel::{BtError, Result, SymbolId, TimeRange, Timestamp};

/// DataStore that returns different bars per symbol.
struct MultiStore {
    bars: HashMap<SymbolId, RecordBatch>,
}

impl MultiStore {
    fn new(bars: HashMap<SymbolId, RecordBatch>) -> Self {
        Self { bars }
    }
}

impl DataStore for MultiStore {
    fn load_bars(
        &self,
        symbol: SymbolId,
        _range: TimeRange,
        _version: Option<&str>,
    ) -> Result<RecordBatch> {
        self.bars
            .get(&symbol)
            .cloned()
            .ok_or_else(|| BtError::Data(format!("no bars for {:?}", symbol)))
    }

    fn load_features(
        &self,
        _feature_set: &str,
        _symbol: SymbolId,
        _range: TimeRange,
        _columns: Option<&[&str]>,
    ) -> Result<RecordBatch> {
        Err(BtError::Unsupported("multi store".to_string()))
    }

    fn load_exo(&self, _name: &str, _range: TimeRange) -> Result<RecordBatch> {
        Err(BtError::Unsupported("multi store".to_string()))
    }

    fn list_versions(&self, _dataset: &str) -> Result<Vec<DatasetVersion>> {
        Ok(vec![mock_version()])
    }

    fn active_version(&self, _dataset: &str) -> Result<DatasetVersion> {
        Ok(mock_version())
    }

    fn write_bars(
        &self,
        _symbol: SymbolId,
        _batch: &RecordBatch,
        _opts: WriteOptions,
    ) -> Result<DatasetVersion> {
        Err(BtError::Unsupported("multi store".to_string()))
    }
}

fn mock_version() -> DatasetVersion {
    DatasetVersion {
        id: "v_test".to_string(),
        dataset: "bars_1m".to_string(),
        created_at: Timestamp::from_nanos(1),
        schema_hash: "hash".to_string(),
        row_count: 4,
        time_range: TimeRange::new(
            Timestamp::from_nanos(0),
            Timestamp::from_nanos(240_000_000_000),
        )
        .expect("range"),
        symbols: vec![SymbolId(1)],
        source_hash: "test".to_string(),
        metadata: serde_json::json!({}),
    }
}

fn sample_bars(symbol_id: u32, closes: &[f64]) -> RecordBatch {
    let schema = bt_data::schema::canonical_bars_1m_schema();
    let n = closes.len();
    let timestamps: Vec<i64> = (0..n as i64).map(|i| i * 60_000_000_000).collect();

    let arrays: Vec<ArrayRef> = vec![
        Arc::new(TimestampNanosecondArray::from(timestamps).with_timezone("UTC")),
        Arc::new(UInt32Array::from(vec![symbol_id; n])),
        Arc::new(Float64Array::from(closes.to_vec())), // open
        Arc::new(Float64Array::from(closes.to_vec())), // high
        Arc::new(Float64Array::from(closes.to_vec())), // low
        Arc::new(Float64Array::from(closes.to_vec())), // close
        Arc::new(Float64Array::from(closes.to_vec())), // vwap
        Arc::new(Float64Array::from(vec![10.0; n])),
        Arc::new(Float64Array::from(
            (0..n).map(|_| Some(5.0)).collect::<Vec<_>>(),
        )),
        Arc::new(Float64Array::from(
            (0..n).map(|_| Some(5.0)).collect::<Vec<_>>(),
        )),
        Arc::new(UInt32Array::from(vec![1_u32; n])),
        Arc::new(Float64Array::from(
            closes.iter().map(|c| Some(c - 0.1)).collect::<Vec<_>>(),
        )),
        Arc::new(Float64Array::from(
            closes.iter().map(|c| Some(c + 0.1)).collect::<Vec<_>>(),
        )),
        Arc::new(Float64Array::from(
            (0..n).map(|_| Some(0.2)).collect::<Vec<_>>(),
        )),
        Arc::new(BooleanArray::from(vec![false; n])),
        Arc::new(UInt8Array::from(vec![0_u8; n])),
    ];

    RecordBatch::try_new(schema, arrays).expect("valid batch")
}

fn half_allocation_strategy() -> StrategyPlan {
    let signal = compile_expr(Expr::Literal(ScalarValue::Float64(1.0))).expect("compile signal");
    let sizing = compile_expr(Expr::Mul(
        Box::new(Expr::Column("signal".to_string())),
        Box::new(Expr::Literal(ScalarValue::Float64(0.5))),
    ))
    .expect("compile sizing");

    StrategyPlan {
        name: "half_alloc".to_string(),
        signals: HashMap::from([("signal".to_string(), signal)]),
        position_sizing: sizing,
        required_columns: vec!["close".to_string()],
        required_features: Vec::new(),
        parameters: HashMap::new(),
        metadata: StrategyMetadata::default(),
        raw_signals: None,
        raw_position_sizing: None,
        has_dynamic_periods: false,
    }
}

fn always_long_strategy() -> StrategyPlan {
    let signal = compile_expr(Expr::Literal(ScalarValue::Float64(1.0))).expect("compile signal");
    let sizing = compile_expr(Expr::Column("signal".to_string())).expect("compile sizing");

    StrategyPlan {
        name: "always_long".to_string(),
        signals: HashMap::from([("signal".to_string(), signal)]),
        position_sizing: sizing,
        required_columns: vec!["close".to_string()],
        required_features: Vec::new(),
        parameters: HashMap::new(),
        metadata: StrategyMetadata::default(),
        raw_signals: None,
        raw_position_sizing: None,
        has_dynamic_periods: false,
    }
}

fn multi_config(universe: Vec<SymbolId>) -> BacktestConfig {
    BacktestConfig {
        universe,
        time_range: TimeRange::new(
            Timestamp::from_nanos(0),
            Timestamp::from_nanos(300_000_000_000),
        )
        .expect("range"),
        bar_interval: BarInterval::Minutes(1),
        initial_capital: 10000.0,
        currency: "USD".to_string(),
        risk_free_rate: 0.025,
        execution: ExecutionConfig {
            signal_delay: 1,
            execution_price: ExecutionPrice::AtClose,
            max_position_pct: 1.0,
            allow_short: false,
            allow_fractional: true,
            skip_gap_bars: false,
            position_sizing_mode: PositionSizingMode::Units,
            fill_model: FillModel::default(),
            orders: OrderConfig::default(),
            pyramiding: false,
        },
        fees: FeeConfig::default(),
        slippage: SlippageConfig::FixedBps { bps: 0.0 },
        data_version: Some("v_test".to_string()),
        rng_seed: Some(42),
        trading_days_per_year: 365.25,
        output_resolution: Some(BarInterval::Minutes(1)),
        resample_to: None,
        feature_sets: vec![],
        symbol_names: HashMap::new(),
        warmup_bars: 0,
    }
}

#[test]
fn two_asset_backtest_produces_correct_equity() {
    let store = MultiStore::new(HashMap::from([
        (SymbolId(1), sample_bars(1, &[100.0, 101.0, 102.0, 103.0])),
        (SymbolId(2), sample_bars(2, &[200.0, 198.0, 202.0, 205.0])),
    ]));
    let config = multi_config(vec![SymbolId(1), SymbolId(2)]);
    let strategy = half_allocation_strategy();

    let result = SimpleBacktestRunner::new()
        .run(&strategy, &config, &store)
        .expect("run");

    let equity = result
        .equity_curve
        .as_any()
        .downcast_ref::<Float64Array>()
        .expect("equity f64");

    assert_eq!(equity.len(), 4);
    assert_eq!(equity.value(0), 10000.0);
    assert_eq!(equity.value(1), 10000.0);
    assert_eq!(equity.value(2), 10002.5);
    assert_eq!(equity.value(3), 10004.5);
}

#[test]
fn two_asset_backtest_produces_trades_for_both_symbols() {
    let store = MultiStore::new(HashMap::from([
        (SymbolId(1), sample_bars(1, &[100.0, 101.0, 102.0, 103.0])),
        (SymbolId(2), sample_bars(2, &[200.0, 198.0, 202.0, 205.0])),
    ]));
    let config = multi_config(vec![SymbolId(1), SymbolId(2)]);
    let strategy = half_allocation_strategy();

    let result = SimpleBacktestRunner::new()
        .run(&strategy, &config, &store)
        .expect("run");

    assert_eq!(result.trades.num_rows(), 2);

    let symbol_ids = result
        .trades
        .column_by_name("symbol_id")
        .expect("sid")
        .as_any()
        .downcast_ref::<UInt32Array>()
        .expect("u32");

    let trade_symbols: Vec<u32> = (0..symbol_ids.len()).map(|i| symbol_ids.value(i)).collect();
    assert!(trade_symbols.contains(&1));
    assert!(trade_symbols.contains(&2));
}

#[test]
fn positions_batch_contains_per_symbol_rows() {
    let store = MultiStore::new(HashMap::from([
        (SymbolId(1), sample_bars(1, &[100.0, 101.0, 102.0, 103.0])),
        (SymbolId(2), sample_bars(2, &[200.0, 198.0, 202.0, 205.0])),
    ]));
    let config = multi_config(vec![SymbolId(1), SymbolId(2)]);
    let strategy = half_allocation_strategy();

    let result = SimpleBacktestRunner::new()
        .run(&strategy, &config, &store)
        .expect("run");

    // 4 bars × 2 symbols = 8 position rows.
    assert_eq!(result.positions.num_rows(), 8);

    let pos_sids = result
        .positions
        .column_by_name("symbol_id")
        .expect("sid")
        .as_any()
        .downcast_ref::<UInt32Array>()
        .expect("u32");
    let pos_positions = result
        .positions
        .column_by_name("position")
        .expect("position")
        .as_any()
        .downcast_ref::<Float64Array>()
        .expect("f64");

    // At row 0 (bar 0), both symbols have position 0.
    assert_eq!(pos_sids.value(0), 1);
    assert_eq!(pos_positions.value(0), 0.0);
    assert_eq!(pos_sids.value(1), 2);
    assert_eq!(pos_positions.value(1), 0.0);

    // At row 1 (bar 1), both symbols have position 0.5.
    assert_eq!(pos_sids.value(2), 1);
    assert_eq!(pos_positions.value(2), 0.5);
    assert_eq!(pos_sids.value(3), 2);
    assert_eq!(pos_positions.value(3), 0.5);
}

#[test]
fn single_asset_regression_unchanged_results() {
    // Single-asset config should produce identical results to before.
    let store = MultiStore::new(HashMap::from([(
        SymbolId(1),
        sample_bars(1, &[100.0, 101.0, 102.0, 103.0]),
    )]));

    let config = multi_config(vec![SymbolId(1)]);
    let strategy = always_long_strategy();

    let result = SimpleBacktestRunner::new()
        .run(&strategy, &config, &store)
        .expect("run");

    let equity = result
        .equity_curve
        .as_any()
        .downcast_ref::<Float64Array>()
        .expect("equity f64");

    // Buy-and-hold single asset: buy 1.0 unit at open=101 on bar 1.
    // capital = 10000 - 101 = 9899. Equity: [10000, 10000, 10001, 10002].
    assert_eq!(equity.len(), 4);
    assert_eq!(equity.value(0), 10000.0);
    assert_eq!(equity.value(1), 10000.0);
    assert_eq!(equity.value(2), 10001.0);
    assert_eq!(equity.value(3), 10002.0);

    assert_eq!(result.trades.num_rows(), 1);
}

#[test]
fn capital_conservation_holds() {
    // Verify: initial_capital + Σ(trade_pnl) = final_equity (no fees/slippage).
    let store = MultiStore::new(HashMap::from([
        (SymbolId(1), sample_bars(1, &[100.0, 101.0, 102.0, 103.0])),
        (SymbolId(2), sample_bars(2, &[200.0, 198.0, 202.0, 205.0])),
    ]));
    let config = multi_config(vec![SymbolId(1), SymbolId(2)]);
    let strategy = half_allocation_strategy();

    let result = SimpleBacktestRunner::new()
        .run(&strategy, &config, &store)
        .expect("run");

    let equity = result
        .equity_curve
        .as_any()
        .downcast_ref::<Float64Array>()
        .expect("equity f64");
    let final_equity = equity.value(equity.len() - 1);

    // With no fees/slippage:
    // PnL = Σ(position[s] × (final_close[s] - entry_price[s]))
    // S1: 0.5 × (103 - 101) = 1.0
    // S2: 0.5 × (205 - 198) = 3.5
    // Expected final equity = 10000 + 1.0 + 3.5 = 10004.5
    let expected = 10000.0 + 0.5 * (103.0 - 101.0) + 0.5 * (205.0 - 198.0);
    assert!(
        (final_equity - expected).abs() < 1e-10,
        "capital conservation failed: got {final_equity}, expected {expected}"
    );
}

#[test]
fn empty_universe_rejected() {
    let store = MultiStore::new(HashMap::new());
    let config = multi_config(vec![]);
    let strategy = half_allocation_strategy();

    let err = SimpleBacktestRunner::new()
        .run(&strategy, &config, &store)
        .expect_err("empty universe");
    assert!(err.to_string().contains("empty"));
}

#[test]
fn duplicate_symbol_rejected() {
    let store = MultiStore::new(HashMap::from([(
        SymbolId(1),
        sample_bars(1, &[100.0, 101.0]),
    )]));

    let config = multi_config(vec![SymbolId(1), SymbolId(1)]);
    let strategy = half_allocation_strategy();

    let err = SimpleBacktestRunner::new()
        .run(&strategy, &config, &store)
        .expect_err("duplicate symbol");
    assert!(err.to_string().contains("duplicate"));
}

const NANOS_PER_DAY: i64 = 86_400_000_000_000;

/// Generate bars spanning multiple days (4 bars per day across 3 days = 12 bars).
fn sample_bars_multiday(symbol_id: u32) -> RecordBatch {
    let schema = bt_data::schema::canonical_bars_1m_schema();
    let bars_per_day = 4;
    let days = 3;
    let n = bars_per_day * days;

    // 4 bars per day, 3 days: day0 [100..103], day1 [104..107], day2 [108..111]
    let closes: Vec<f64> = (0..n).map(|i| 100.0 + i as f64).collect();
    let timestamps: Vec<i64> = (0..n as i64)
        .map(|i| {
            let day = i / bars_per_day as i64;
            let intra = i % bars_per_day as i64;
            day * NANOS_PER_DAY + intra * 60_000_000_000
        })
        .collect();

    let arrays: Vec<ArrayRef> = vec![
        Arc::new(TimestampNanosecondArray::from(timestamps).with_timezone("UTC")),
        Arc::new(UInt32Array::from(vec![symbol_id; n])),
        Arc::new(Float64Array::from(closes.clone())), // open
        Arc::new(Float64Array::from(closes.clone())), // high
        Arc::new(Float64Array::from(closes.clone())), // low
        Arc::new(Float64Array::from(closes.clone())), // close
        Arc::new(Float64Array::from(closes.clone())), // vwap
        Arc::new(Float64Array::from(vec![10.0; n])),
        Arc::new(Float64Array::from(
            (0..n).map(|_| Some(5.0)).collect::<Vec<_>>(),
        )),
        Arc::new(Float64Array::from(
            (0..n).map(|_| Some(5.0)).collect::<Vec<_>>(),
        )),
        Arc::new(UInt32Array::from(vec![1_u32; n])),
        Arc::new(Float64Array::from(
            closes.iter().map(|c| Some(c - 0.1)).collect::<Vec<_>>(),
        )),
        Arc::new(Float64Array::from(
            closes.iter().map(|c| Some(c + 0.1)).collect::<Vec<_>>(),
        )),
        Arc::new(Float64Array::from(
            (0..n).map(|_| Some(0.2)).collect::<Vec<_>>(),
        )),
        Arc::new(BooleanArray::from(vec![false; n])),
        Arc::new(UInt8Array::from(vec![0_u8; n])),
    ];

    RecordBatch::try_new(schema, arrays).expect("valid batch")
}

#[test]
fn output_resolution_daily_downsamples_equity_and_positions() {
    let store = MultiStore::new(HashMap::from([(SymbolId(1), sample_bars_multiday(1))]));

    let mut config = multi_config(vec![SymbolId(1)]);
    config.time_range = TimeRange::new(
        Timestamp::from_nanos(0),
        Timestamp::from_nanos(3 * NANOS_PER_DAY),
    )
    .expect("range");
    config.output_resolution = Some(BarInterval::Days(1));

    let strategy = always_long_strategy();
    let result = SimpleBacktestRunner::new()
        .run(&strategy, &config, &store)
        .expect("run");

    // 12 bars across 3 days → daily downsampling should yield ~4 rows
    // (first row + last of day 0 + last of day 1 + last of day 2).
    let equity = result
        .equity_curve
        .as_any()
        .downcast_ref::<Float64Array>()
        .expect("equity f64");

    println!(
        "equity len = {}, values = {:?}",
        equity.len(),
        (0..equity.len())
            .map(|i| equity.value(i))
            .collect::<Vec<_>>()
    );

    // Should have far fewer rows than the 12 input bars.
    assert!(
        equity.len() < 12,
        "expected downsampled equity, got {} rows",
        equity.len()
    );
    // But at least 2 (first + last).
    assert!(equity.len() >= 2);

    // First equity = initial capital.
    assert_eq!(equity.value(0), 10000.0);
    // Last equity should match full-res last equity (same final state).
    let last_eq = equity.value(equity.len() - 1);
    assert!(last_eq > 10000.0, "should have positive PnL");

    // Positions should also be downsampled.
    let pos_rows = result.positions.num_rows();
    println!(
        "positions rows = {} (1 symbol × {} sampled bars)",
        pos_rows,
        equity.len()
    );
    assert_eq!(pos_rows, equity.len()); // 1 symbol → same count as equity

    // daily_returns should be actual daily returns (independent of output downsampling).
    let dr = result
        .daily_returns
        .as_any()
        .downcast_ref::<Float64Array>()
        .expect("daily returns f64");
    println!(
        "daily_returns len = {}, values = {:?}",
        dr.len(),
        (0..dr.len()).map(|i| dr.value(i)).collect::<Vec<_>>()
    );
    // 3 days → 2 daily returns.
    assert_eq!(dr.len(), 2);
}

// ---------------------------------------------------------------------------
// Cross-sectional 2-pass integration tests
// ---------------------------------------------------------------------------

/// Strategy with cross-sectional rank: sizes position by CrossSectionalRank(close).
fn cross_section_rank_strategy() -> StrategyPlan {
    // Signal "momentum": just the close price (a simple proxy for momentum).
    let momentum = compile_expr(Expr::Column("close".to_string())).expect("compile momentum");

    // Signal "cs_rank": CrossSectionalRank of "momentum"
    let cs_rank = compile_expr(Expr::CrossSectionalRank(Box::new(Expr::Column(
        "momentum".to_string(),
    ))))
    .expect("compile cs_rank");

    // Position sizing: use the cross-sectional rank directly.
    let sizing = compile_expr(Expr::Column("cs_rank".to_string())).expect("compile sizing");

    StrategyPlan {
        name: "cs_rank_strategy".to_string(),
        signals: HashMap::from([
            ("momentum".to_string(), momentum),
            ("cs_rank".to_string(), cs_rank),
        ]),
        position_sizing: sizing,
        required_columns: vec!["close".to_string()],
        required_features: Vec::new(),
        parameters: HashMap::new(),
        metadata: StrategyMetadata::default(),
        raw_signals: None,
        raw_position_sizing: None,
        has_dynamic_periods: false,
    }
}

#[test]
fn cross_sectional_rank_produces_meaningful_positions() {
    // Symbol 1 close: [100, 101, 102, 103] (monotonically increasing)
    // Symbol 2 close: [200, 198, 202, 205] (higher absolute values)
    //
    // At each bar, CrossSectionalRank(close) with 2 symbols produces:
    // - The symbol with lower close → rank 0.0
    // - The symbol with higher close → rank 1.0
    //
    // Symbol 2 has higher closes throughout, so:
    //   Symbol 1 → rank 0.0, Symbol 2 → rank 1.0
    // Position sizing = rank, so Symbol 1 buys 0 units, Symbol 2 buys 1 unit.
    let store = MultiStore::new(HashMap::from([
        (SymbolId(1), sample_bars(1, &[100.0, 101.0, 102.0, 103.0])),
        (SymbolId(2), sample_bars(2, &[200.0, 198.0, 202.0, 205.0])),
    ]));

    let config = multi_config(vec![SymbolId(1), SymbolId(2)]);
    let strategy = cross_section_rank_strategy();

    let result = SimpleBacktestRunner::new()
        .run(&strategy, &config, &store)
        .expect("run with cross-sectional rank");

    // Should have at least 1 trade (Symbol 2 should trade, Symbol 1 has rank 0 so no position).
    assert!(result.trades.num_rows() >= 1);

    let trade_sids = result
        .trades
        .column_by_name("symbol_id")
        .expect("sid")
        .as_any()
        .downcast_ref::<UInt32Array>()
        .expect("u32");

    // All trades should be for symbol 2 (the one with rank 1.0).
    for i in 0..trade_sids.len() {
        assert_eq!(
            trade_sids.value(i),
            2,
            "expected trades only for symbol 2 (higher rank), but got symbol {}",
            trade_sids.value(i)
        );
    }

    // Equity should show positive PnL from symbol 2's position.
    let equity = result
        .equity_curve
        .as_any()
        .downcast_ref::<Float64Array>()
        .expect("equity f64");
    assert!(
        equity.value(equity.len() - 1) > 10000.0,
        "expected positive PnL"
    );
}

/// Strategy with cross-sectional mean.
fn cross_section_mean_strategy() -> StrategyPlan {
    // Signal "close_val": just the close price.
    let close_val = compile_expr(Expr::Column("close".to_string())).expect("compile");

    // Signal "cs_mean": CrossSectionalMean of "close_val"
    let cs_mean = compile_expr(Expr::CrossSectionalMean(Box::new(Expr::Column(
        "close_val".to_string(),
    ))))
    .expect("compile cs_mean");

    // Position sizing: buy 1 unit if close > cs_mean (above-average performer).
    let sizing = compile_expr(Expr::IfElse(
        Box::new(Expr::Gt(
            Box::new(Expr::Column("close".to_string())),
            Box::new(Expr::Column("cs_mean".to_string())),
        )),
        Box::new(Expr::Literal(ScalarValue::Float64(1.0))),
        Box::new(Expr::Literal(ScalarValue::Float64(0.0))),
    ))
    .expect("compile sizing");

    StrategyPlan {
        name: "cs_mean_strategy".to_string(),
        signals: HashMap::from([
            ("close_val".to_string(), close_val),
            ("cs_mean".to_string(), cs_mean),
        ]),
        position_sizing: sizing,
        required_columns: vec!["close".to_string()],
        required_features: Vec::new(),
        parameters: HashMap::new(),
        metadata: StrategyMetadata::default(),
        raw_signals: None,
        raw_position_sizing: None,
        has_dynamic_periods: false,
    }
}

#[test]
fn cross_sectional_mean_filters_above_average() {
    // Symbol 1 close: [100, 101, 102, 103]
    // Symbol 2 close: [200, 198, 202, 205]
    //
    // Cross-sectional mean at each bar: [150, 149.5, 152, 154]
    // Symbol 2 is always above the mean → gets position 1.0
    // Symbol 1 is always below the mean → gets position 0.0
    let store = MultiStore::new(HashMap::from([
        (SymbolId(1), sample_bars(1, &[100.0, 101.0, 102.0, 103.0])),
        (SymbolId(2), sample_bars(2, &[200.0, 198.0, 202.0, 205.0])),
    ]));

    let config = multi_config(vec![SymbolId(1), SymbolId(2)]);
    let strategy = cross_section_mean_strategy();

    let result = SimpleBacktestRunner::new()
        .run(&strategy, &config, &store)
        .expect("run with cross-sectional mean");

    // Only Symbol 2 should have trades (above average).
    let trade_sids = result
        .trades
        .column_by_name("symbol_id")
        .expect("sid")
        .as_any()
        .downcast_ref::<UInt32Array>()
        .expect("u32");

    for i in 0..trade_sids.len() {
        assert_eq!(
            trade_sids.value(i),
            2,
            "expected trades only for symbol 2 (above-average), got symbol {}",
            trade_sids.value(i)
        );
    }
}

// ---------------------------------------------------------------------------
// SymbolRef tests
// ---------------------------------------------------------------------------

/// Strategy where all symbols use symbol 1's close as a signal.
fn symbol_ref_strategy() -> StrategyPlan {
    // "btc_close" signal: SymbolRef("1", Column("close"))
    // Uses SymbolId fallback naming: symbol_id.0.to_string()
    let btc_close = compile_expr(Expr::SymbolRef(
        "1".to_string(),
        Box::new(Expr::Column("close".to_string())),
    ))
    .expect("compile btc_close");

    // Position sizing: if btc_close > 101 then 1.0 else 0.0
    let sizing = compile_expr(Expr::IfElse(
        Box::new(Expr::Gt(
            Box::new(Expr::Column("btc_close".to_string())),
            Box::new(Expr::Literal(ScalarValue::Float64(101.0))),
        )),
        Box::new(Expr::Literal(ScalarValue::Float64(1.0))),
        Box::new(Expr::Literal(ScalarValue::Float64(0.0))),
    ))
    .expect("compile sizing");

    StrategyPlan {
        name: "symbol_ref_test".to_string(),
        signals: HashMap::from([("btc_close".to_string(), btc_close)]),
        position_sizing: sizing,
        required_columns: vec!["close".to_string()],
        required_features: Vec::new(),
        parameters: HashMap::new(),
        metadata: StrategyMetadata::default(),
        raw_signals: None,
        raw_position_sizing: None,
        has_dynamic_periods: false,
    }
}

#[test]
fn symbol_ref_injects_other_symbol_data() {
    // Symbol 1 (BTC): rising prices — crosses 101 at bar 2
    // Symbol 2 (ETH): independent prices
    //
    // btc_close signal = Symbol 1's close for ALL symbols.
    // Sizing: buy 1 unit if btc_close > 101.
    // With signal_delay=1:
    //   signal at bar 2 (btc_close=102 > 101) → target=1, fills at bar 3
    //   Position held bars 3-5, PnL accrues from price movement.
    let store = MultiStore::new(HashMap::from([
        (
            SymbolId(1),
            sample_bars(1, &[100.0, 101.0, 102.0, 103.0, 105.0, 108.0]),
        ),
        (
            SymbolId(2),
            sample_bars(2, &[200.0, 198.0, 202.0, 205.0, 210.0, 215.0]),
        ),
    ]));
    let mut config = multi_config(vec![SymbolId(1), SymbolId(2)]);
    config.time_range = TimeRange::new(
        Timestamp::from_nanos(0),
        Timestamp::from_nanos(420_000_000_000),
    )
    .expect("range");
    let strategy = symbol_ref_strategy();

    let result = SimpleBacktestRunner::new()
        .run(&strategy, &config, &store)
        .expect("run with symbol_ref");

    // Both symbols should have trades — they both use BTC's close as signal.
    assert!(
        result.trades.num_rows() >= 1,
        "expected at least one trade, got {}",
        result.trades.num_rows()
    );

    // Final equity should differ from initial capital (positions accrued PnL).
    let equity = result
        .equity_curve
        .as_any()
        .downcast_ref::<Float64Array>()
        .unwrap();
    let final_equity = equity.value(equity.len() - 1);
    assert!(
        (final_equity - 10000.0).abs() > 1e-6,
        "expected equity to differ from initial capital after SymbolRef-driven trades, got {}",
        final_equity
    );
}
