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, ParamGrid, 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};

struct MockStore {
    bars: RecordBatch,
    version: DatasetVersion,
}

impl MockStore {
    fn new(bars: RecordBatch) -> Self {
        Self {
            bars,
            version: DatasetVersion {
                id: "v_mock".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("valid range"),
                symbols: vec![SymbolId(1)],
                source_hash: "source".to_string(),
                metadata: serde_json::json!({}),
            },
        }
    }
}

impl DataStore for MockStore {
    fn load_bars(
        &self,
        _symbol: SymbolId,
        _range: TimeRange,
        _version: Option<&str>,
    ) -> Result<RecordBatch> {
        Ok(self.bars.clone())
    }

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

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

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

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

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

#[test]
fn single_asset_buy_and_hold_produces_expected_equity() {
    let store = MockStore::new(sample_bars(false, false));
    let strategy = strategy_always_long();
    let runner = SimpleBacktestRunner::new();
    let config = test_config(
        ExecutionPrice::AtClose,
        SlippageConfig::FixedBps { bps: 0.0 },
    );

    let result = runner
        .run(&strategy, &config, &store)
        .expect("run backtest");

    let equity = result
        .equity_curve
        .as_any()
        .downcast_ref::<Float64Array>()
        .expect("equity float64");
    assert_eq!(equity.len(), 4);
    assert_eq!(equity.value(0), 1000.0);
    assert_eq!(equity.value(1), 1000.0);
    assert_eq!(equity.value(2), 1001.0);
    assert_eq!(equity.value(3), 1002.0);

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

#[test]
fn midprice_and_spread_fallback_warnings_are_emitted() {
    let store = MockStore::new(sample_bars(true, true));
    let strategy = strategy_always_long();
    let runner = SimpleBacktestRunner::new();
    let config = test_config(
        ExecutionPrice::MidPrice,
        SlippageConfig::SpreadBased {
            spread_fraction: 1.0,
        },
    );

    let result = runner
        .run(&strategy, &config, &store)
        .expect("run backtest");

    assert!(result
        .warnings
        .iter()
        .any(|warning| warning.contains("mid-price fallback")));
    assert!(result
        .warnings
        .iter()
        .any(|warning| warning.contains("fallback to FixedBpsSlippage(5.0)")));
}

#[test]
fn run_sweep_parallel_returns_one_result_per_combo() {
    let store = MockStore::new(sample_bars(false, false));
    let runner = SimpleBacktestRunner::new();
    let config = test_config(
        ExecutionPrice::AtClose,
        SlippageConfig::FixedBps { bps: 0.0 },
    );

    let strategy = strategy_param_sized();
    let grid = ParamGrid::from([(
        "size".to_string(),
        vec![
            ScalarValue::Float64(0.5),
            ScalarValue::Float64(1.0),
            ScalarValue::Float64(1.5),
        ],
    )]);

    let results = runner
        .run_sweep(&strategy, &grid, &config, &store, 2)
        .expect("run sweep");
    assert_eq!(results.len(), 3);

    let mut quantities = Vec::new();
    for result in results {
        let quantity = result
            .trades
            .column_by_name("quantity")
            .expect("quantity")
            .as_any()
            .downcast_ref::<Float64Array>()
            .expect("quantity float")
            .value(0);
        quantities.push(quantity);
    }

    assert_eq!(quantities, vec![0.5, 1.0, 1.5]);
}

#[test]
fn run_sweep_rejects_zero_parallelism() {
    let store = MockStore::new(sample_bars(false, false));
    let runner = SimpleBacktestRunner::new();
    let config = test_config(
        ExecutionPrice::AtClose,
        SlippageConfig::FixedBps { bps: 0.0 },
    );
    let strategy = strategy_param_sized();
    let grid = ParamGrid::from([("size".to_string(), vec![ScalarValue::Float64(1.0)])]);

    let error = runner
        .run_sweep(&strategy, &grid, &config, &store, 0)
        .expect_err("zero parallelism rejected");
    assert!(error.to_string().contains("max_parallelism"));
}

fn strategy_always_long() -> 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");

    let mut signals = HashMap::new();
    signals.insert("signal".to_string(), signal);

    StrategyPlan {
        name: "always_long".to_string(),
        signals,
        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 strategy_param_sized() -> 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::Parameter("size".to_string())),
    ))
    .expect("compile sizing");

    let mut signals = HashMap::new();
    signals.insert("signal".to_string(), signal);

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

fn test_config(execution_price: ExecutionPrice, slippage: SlippageConfig) -> BacktestConfig {
    BacktestConfig {
        universe: vec![SymbolId(1)],
        time_range: TimeRange::new(
            Timestamp::from_nanos(0),
            Timestamp::from_nanos(300_000_000_000),
        )
        .expect("valid range"),
        bar_interval: BarInterval::Minutes(1),
        initial_capital: 1000.0,
        currency: "USD".to_string(),
        risk_free_rate: 0.025,
        execution: ExecutionConfig {
            signal_delay: 1,
            execution_price,
            max_position_pct: 1.0,
            allow_short: false,
            allow_fractional: true,
            skip_gap_bars: false,
            position_sizing_mode: PositionSizingMode::Units,
            fill_model: FillModel::default(),
            ..Default::default()
        },
        fees: FeeConfig {
            taker_fee_bps: 0.0,
            ..FeeConfig::default()
        },
        slippage,
        data_version: None,
        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,
    }
}

fn sample_bars(null_quotes: bool, null_spread: bool) -> RecordBatch {
    let schema = bt_data::schema::canonical_bars_1m_schema();
    let timestamps = vec![0_i64, 60_000_000_000, 120_000_000_000, 180_000_000_000];
    let symbol_id = vec![1_u32; 4];

    let bid = if null_quotes {
        vec![None, None, None, None]
    } else {
        vec![Some(99.9), Some(100.9), Some(101.9), Some(102.9)]
    };
    let ask = if null_quotes {
        vec![None, None, None, None]
    } else {
        vec![Some(100.1), Some(101.1), Some(102.1), Some(103.1)]
    };
    let spread = if null_spread {
        vec![None, None, None, None]
    } else {
        vec![Some(0.2), Some(0.2), Some(0.2), Some(0.2)]
    };

    let arrays: Vec<ArrayRef> = vec![
        Arc::new(TimestampNanosecondArray::from(timestamps).with_timezone("UTC")),
        Arc::new(UInt32Array::from(symbol_id)),
        Arc::new(Float64Array::from(vec![100.0, 101.0, 102.0, 103.0])),
        Arc::new(Float64Array::from(vec![100.0, 101.0, 102.0, 103.0])),
        Arc::new(Float64Array::from(vec![100.0, 101.0, 102.0, 103.0])),
        Arc::new(Float64Array::from(vec![100.0, 101.0, 102.0, 103.0])),
        Arc::new(Float64Array::from(vec![100.0, 101.0, 102.0, 103.0])),
        Arc::new(Float64Array::from(vec![10.0, 10.0, 10.0, 10.0])),
        Arc::new(Float64Array::from(vec![
            Some(5.0),
            Some(5.0),
            Some(5.0),
            Some(5.0),
        ])),
        Arc::new(Float64Array::from(vec![
            Some(5.0),
            Some(5.0),
            Some(5.0),
            Some(5.0),
        ])),
        Arc::new(UInt32Array::from(vec![1_u32, 1, 1, 1])),
        Arc::new(Float64Array::from(bid)),
        Arc::new(Float64Array::from(ask)),
        Arc::new(Float64Array::from(spread)),
        Arc::new(BooleanArray::from(vec![false, false, false, false])),
        Arc::new(UInt8Array::from(vec![0_u8, 0, 0, 0])),
    ];

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

/// Run same strategy+data through fast path and general path, compare bit-for-bit.
/// General path is forced by setting `skip_gap_bars: true` which disqualifies the
/// fast path but has no effect when there are no gap bars in the data.
#[test]
fn fast_path_matches_general_path() {
    let store = MockStore::new(sample_bars(false, false));
    let strategy = strategy_always_long();
    let runner = SimpleBacktestRunner::new();

    // Fast path config (qualifies).
    let config_fast = test_config(
        ExecutionPrice::AtClose,
        SlippageConfig::FixedBps { bps: 5.0 },
    );
    let result_fast = runner.run(&strategy, &config_fast, &store).expect("fast");

    // General path config: add skip_gap_bars to disqualify fast path.
    let mut config_general = config_fast.clone();
    config_general.execution.skip_gap_bars = true;
    let result_general = runner
        .run(&strategy, &config_general, &store)
        .expect("general");

    // Compare equity curves.
    let eq_fast = result_fast
        .equity_curve
        .as_any()
        .downcast_ref::<Float64Array>()
        .unwrap();
    let eq_general = result_general
        .equity_curve
        .as_any()
        .downcast_ref::<Float64Array>()
        .unwrap();
    assert_eq!(eq_fast.len(), eq_general.len(), "equity length mismatch");
    for i in 0..eq_fast.len() {
        assert!(
            (eq_fast.value(i) - eq_general.value(i)).abs() < 1e-10,
            "equity mismatch at bar {}: fast={} general={}",
            i,
            eq_fast.value(i),
            eq_general.value(i),
        );
    }

    // Compare trade counts.
    assert_eq!(
        result_fast.trades.num_rows(),
        result_general.trades.num_rows(),
        "trade count mismatch"
    );

    // Compare key metrics.
    assert!(
        (result_fast.metrics.total_return - result_general.metrics.total_return).abs() < 1e-10,
        "total_return mismatch"
    );
    assert!(
        (result_fast.metrics.max_drawdown - result_general.metrics.max_drawdown).abs() < 1e-10,
        "max_drawdown mismatch"
    );
}

#[test]
fn fast_path_matches_general_with_fees() {
    let store = MockStore::new(sample_bars(false, false));
    let strategy = strategy_always_long();
    let runner = SimpleBacktestRunner::new();

    let mut config_fast = test_config(
        ExecutionPrice::AtClose,
        SlippageConfig::FixedBps { bps: 10.0 },
    );
    config_fast.fees.taker_fee_bps = 30.0;
    config_fast.fees.min_fee = 0.01;

    let result_fast = runner.run(&strategy, &config_fast, &store).expect("fast");

    let mut config_general = config_fast.clone();
    config_general.execution.skip_gap_bars = true;
    let result_general = runner
        .run(&strategy, &config_general, &store)
        .expect("general");

    let eq_fast = result_fast
        .equity_curve
        .as_any()
        .downcast_ref::<Float64Array>()
        .unwrap();
    let eq_general = result_general
        .equity_curve
        .as_any()
        .downcast_ref::<Float64Array>()
        .unwrap();
    for i in 0..eq_fast.len() {
        assert!(
            (eq_fast.value(i) - eq_general.value(i)).abs() < 1e-10,
            "equity mismatch at bar {} (with fees): fast={} general={}",
            i,
            eq_fast.value(i),
            eq_general.value(i),
        );
    }
}
