use chrono::NaiveDate;
use fidc_core::{
    BacktestConfig, BacktestEngine, BenchmarkSnapshot, BrokerSimulator, CandidateEligibility,
    ChinaAShareCostModel, ChinaEquityRuleHooks, CorporateAction, DailyFactorSnapshot,
    DailyMarketSnapshot, DataSet, Instrument, OrderIntent, PriceField, Strategy, StrategyContext,
    StrategyDecision,
};
use std::collections::{BTreeMap, BTreeSet};

fn d(year: i32, month: u32, day: u32) -> NaiveDate {
    NaiveDate::from_ymd_opt(year, month, day).expect("valid date")
}

struct BuyThenHoldStrategy;

impl Strategy for BuyThenHoldStrategy {
    fn name(&self) -> &str {
        "buy-then-hold"
    }

    fn on_day(
        &mut self,
        ctx: &StrategyContext<'_>,
    ) -> Result<StrategyDecision, fidc_core::BacktestError> {
        if ctx.decision_date == d(2025, 1, 2) && ctx.portfolio.position("000001.SZ").is_none() {
            return Ok(StrategyDecision {
                rebalance: false,
                target_weights: BTreeMap::new(),
                exit_symbols: BTreeSet::new(),
                order_intents: vec![OrderIntent::Value {
                    symbol: "000001.SZ".to_string(),
                    value: 10_000.0,
                    reason: "seed_position".to_string(),
                }],
                notes: Vec::new(),
                diagnostics: Vec::new(),
            });
        }
        Ok(StrategyDecision::default())
    }
}

#[test]
fn engine_settles_delisted_position_before_missing_market_snapshot_breaks_run() {
    let date1 = d(2025, 1, 2);
    let date2 = d(2025, 1, 3);
    let data = DataSet::from_components(
        vec![
            Instrument {
                symbol: "000001.SZ".to_string(),
                name: "Delisted".to_string(),
                board: "SZ".to_string(),
                round_lot: 100,
                listed_at: Some(d(2020, 1, 1)),
                delisted_at: Some(date1),
                status: "delisted".to_string(),
            },
            Instrument {
                symbol: "000002.SZ".to_string(),
                name: "Anchor".to_string(),
                board: "SZ".to_string(),
                round_lot: 100,
                listed_at: Some(d(2020, 1, 1)),
                delisted_at: None,
                status: "active".to_string(),
            },
        ],
        vec![
            DailyMarketSnapshot {
                date: date1,
                symbol: "000001.SZ".to_string(),
                timestamp: Some("2025-01-02 10:18:00".to_string()),
                day_open: 10.0,
                open: 10.0,
                high: 10.0,
                low: 10.0,
                close: 10.0,
                last_price: 10.0,
                bid1: 10.0,
                ask1: 10.0,
                prev_close: 10.0,
                volume: 100_000,
                tick_volume: 100_000,
                bid1_volume: 100_000,
                ask1_volume: 100_000,
                trading_phase: Some("continuous".to_string()),
                paused: false,
                upper_limit: 11.0,
                lower_limit: 9.0,
                price_tick: 0.01,
            },
            DailyMarketSnapshot {
                date: date1,
                symbol: "000002.SZ".to_string(),
                timestamp: Some("2025-01-02 10:18:00".to_string()),
                day_open: 5.0,
                open: 5.0,
                high: 5.1,
                low: 4.9,
                close: 5.0,
                last_price: 5.0,
                bid1: 4.99,
                ask1: 5.01,
                prev_close: 5.0,
                volume: 100_000,
                tick_volume: 100_000,
                bid1_volume: 100_000,
                ask1_volume: 100_000,
                trading_phase: Some("continuous".to_string()),
                paused: false,
                upper_limit: 5.5,
                lower_limit: 4.5,
                price_tick: 0.01,
            },
            DailyMarketSnapshot {
                date: date2,
                symbol: "000002.SZ".to_string(),
                timestamp: Some("2025-01-03 10:18:00".to_string()),
                day_open: 5.1,
                open: 5.1,
                high: 5.2,
                low: 5.0,
                close: 5.1,
                last_price: 5.1,
                bid1: 5.09,
                ask1: 5.11,
                prev_close: 5.0,
                volume: 120_000,
                tick_volume: 120_000,
                bid1_volume: 120_000,
                ask1_volume: 120_000,
                trading_phase: Some("continuous".to_string()),
                paused: false,
                upper_limit: 5.5,
                lower_limit: 4.5,
                price_tick: 0.01,
            },
        ],
        vec![
            DailyFactorSnapshot {
                date: date1,
                symbol: "000001.SZ".to_string(),
                market_cap_bn: 20.0,
                free_float_cap_bn: 18.0,
                pe_ttm: 10.0,
                turnover_ratio: Some(1.0),
                effective_turnover_ratio: Some(1.0),
                extra_factors: BTreeMap::new(),
            },
            DailyFactorSnapshot {
                date: date1,
                symbol: "000002.SZ".to_string(),
                market_cap_bn: 30.0,
                free_float_cap_bn: 28.0,
                pe_ttm: 10.0,
                turnover_ratio: Some(1.0),
                effective_turnover_ratio: Some(1.0),
                extra_factors: BTreeMap::new(),
            },
            DailyFactorSnapshot {
                date: date2,
                symbol: "000002.SZ".to_string(),
                market_cap_bn: 31.0,
                free_float_cap_bn: 29.0,
                pe_ttm: 10.0,
                turnover_ratio: Some(1.0),
                effective_turnover_ratio: Some(1.0),
                extra_factors: BTreeMap::new(),
            },
        ],
        vec![
            CandidateEligibility {
                date: date1,
                symbol: "000001.SZ".to_string(),
                is_st: false,
                is_new_listing: false,
                is_paused: false,
                allow_buy: true,
                allow_sell: true,
                is_kcb: false,
                is_one_yuan: false,
            },
            CandidateEligibility {
                date: date1,
                symbol: "000002.SZ".to_string(),
                is_st: false,
                is_new_listing: false,
                is_paused: false,
                allow_buy: true,
                allow_sell: true,
                is_kcb: false,
                is_one_yuan: false,
            },
            CandidateEligibility {
                date: date2,
                symbol: "000002.SZ".to_string(),
                is_st: false,
                is_new_listing: false,
                is_paused: false,
                allow_buy: true,
                allow_sell: true,
                is_kcb: false,
                is_one_yuan: false,
            },
        ],
        vec![
            BenchmarkSnapshot {
                date: date1,
                benchmark: "000300.SH".to_string(),
                open: 100.0,
                close: 100.0,
                prev_close: 99.0,
                volume: 1_000_000,
            },
            BenchmarkSnapshot {
                date: date2,
                benchmark: "000300.SH".to_string(),
                open: 101.0,
                close: 101.0,
                prev_close: 100.0,
                volume: 1_100_000,
            },
        ],
    )
    .expect("dataset");

    let broker = BrokerSimulator::new_with_execution_price(
        ChinaAShareCostModel::default(),
        ChinaEquityRuleHooks::default(),
        PriceField::Open,
    );
    let mut engine = BacktestEngine::new(
        data,
        BuyThenHoldStrategy,
        broker,
        BacktestConfig {
            initial_cash: 100_000.0,
            benchmark_code: "000300.SH".to_string(),
            start_date: Some(date1),
            end_date: Some(date2),
            decision_lag_trading_days: 0,
            execution_price_field: PriceField::Open,
        },
    );

    let result = engine.run().expect("backtest succeeds");
    assert_eq!(result.fills.len(), 2);
    assert!(
        result
            .fills
            .iter()
            .any(|fill| fill.reason.contains("delisted_cash_settlement")
                && fill.symbol == "000001.SZ")
    );
    assert!(
        result
            .holdings_summary
            .iter()
            .all(|holding| holding.symbol != "000001.SZ")
    );
}

#[test]
fn engine_applies_successor_conversion_before_delisted_cash_settlement() {
    let date1 = d(2025, 1, 2);
    let date2 = d(2025, 1, 3);
    let data = DataSet::from_components_with_actions(
        vec![
            Instrument {
                symbol: "000001.SZ".to_string(),
                name: "Legacy".to_string(),
                board: "SZ".to_string(),
                round_lot: 100,
                listed_at: Some(d(2020, 1, 1)),
                delisted_at: Some(date2),
                status: "delisted".to_string(),
            },
            Instrument {
                symbol: "000002.SZ".to_string(),
                name: "Successor".to_string(),
                board: "SZ".to_string(),
                round_lot: 100,
                listed_at: Some(d(2020, 1, 1)),
                delisted_at: None,
                status: "active".to_string(),
            },
        ],
        vec![
            DailyMarketSnapshot {
                date: date1,
                symbol: "000001.SZ".to_string(),
                timestamp: Some("2025-01-02 10:18:00".to_string()),
                day_open: 10.0,
                open: 10.0,
                high: 10.0,
                low: 10.0,
                close: 10.0,
                last_price: 10.0,
                bid1: 10.0,
                ask1: 10.0,
                prev_close: 10.0,
                volume: 100_000,
                tick_volume: 100_000,
                bid1_volume: 100_000,
                ask1_volume: 100_000,
                trading_phase: Some("continuous".to_string()),
                paused: false,
                upper_limit: 11.0,
                lower_limit: 9.0,
                price_tick: 0.01,
            },
            DailyMarketSnapshot {
                date: date1,
                symbol: "000002.SZ".to_string(),
                timestamp: Some("2025-01-02 10:18:00".to_string()),
                day_open: 20.0,
                open: 20.0,
                high: 20.0,
                low: 20.0,
                close: 20.0,
                last_price: 20.0,
                bid1: 20.0,
                ask1: 20.0,
                prev_close: 20.0,
                volume: 100_000,
                tick_volume: 100_000,
                bid1_volume: 100_000,
                ask1_volume: 100_000,
                trading_phase: Some("continuous".to_string()),
                paused: false,
                upper_limit: 22.0,
                lower_limit: 18.0,
                price_tick: 0.01,
            },
            DailyMarketSnapshot {
                date: date2,
                symbol: "000002.SZ".to_string(),
                timestamp: Some("2025-01-03 10:18:00".to_string()),
                day_open: 21.0,
                open: 21.0,
                high: 21.0,
                low: 21.0,
                close: 21.0,
                last_price: 21.0,
                bid1: 21.0,
                ask1: 21.0,
                prev_close: 20.0,
                volume: 120_000,
                tick_volume: 120_000,
                bid1_volume: 120_000,
                ask1_volume: 120_000,
                trading_phase: Some("continuous".to_string()),
                paused: false,
                upper_limit: 22.0,
                lower_limit: 18.0,
                price_tick: 0.01,
            },
        ],
        vec![
            DailyFactorSnapshot {
                date: date1,
                symbol: "000001.SZ".to_string(),
                market_cap_bn: 20.0,
                free_float_cap_bn: 18.0,
                pe_ttm: 10.0,
                turnover_ratio: Some(1.0),
                effective_turnover_ratio: Some(1.0),
                extra_factors: BTreeMap::new(),
            },
            DailyFactorSnapshot {
                date: date1,
                symbol: "000002.SZ".to_string(),
                market_cap_bn: 30.0,
                free_float_cap_bn: 28.0,
                pe_ttm: 10.0,
                turnover_ratio: Some(1.0),
                effective_turnover_ratio: Some(1.0),
                extra_factors: BTreeMap::new(),
            },
            DailyFactorSnapshot {
                date: date2,
                symbol: "000002.SZ".to_string(),
                market_cap_bn: 31.0,
                free_float_cap_bn: 29.0,
                pe_ttm: 10.0,
                turnover_ratio: Some(1.0),
                effective_turnover_ratio: Some(1.0),
                extra_factors: BTreeMap::new(),
            },
        ],
        vec![
            CandidateEligibility {
                date: date1,
                symbol: "000001.SZ".to_string(),
                is_st: false,
                is_new_listing: false,
                is_paused: false,
                allow_buy: true,
                allow_sell: true,
                is_kcb: false,
                is_one_yuan: false,
            },
            CandidateEligibility {
                date: date1,
                symbol: "000002.SZ".to_string(),
                is_st: false,
                is_new_listing: false,
                is_paused: false,
                allow_buy: true,
                allow_sell: true,
                is_kcb: false,
                is_one_yuan: false,
            },
            CandidateEligibility {
                date: date2,
                symbol: "000002.SZ".to_string(),
                is_st: false,
                is_new_listing: false,
                is_paused: false,
                allow_buy: true,
                allow_sell: true,
                is_kcb: false,
                is_one_yuan: false,
            },
        ],
        vec![
            BenchmarkSnapshot {
                date: date1,
                benchmark: "000300.SH".to_string(),
                open: 100.0,
                close: 100.0,
                prev_close: 99.0,
                volume: 1_000_000,
            },
            BenchmarkSnapshot {
                date: date2,
                benchmark: "000300.SH".to_string(),
                open: 101.0,
                close: 101.0,
                prev_close: 100.0,
                volume: 1_100_000,
            },
        ],
        vec![CorporateAction {
            date: date2,
            symbol: "000001.SZ".to_string(),
            payable_date: None,
            share_cash: 0.0,
            share_bonus: 0.0,
            share_gift: 0.0,
            issue_quantity: 0.0,
            issue_price: 0.0,
            reform: false,
            adjust_factor: None,
            successor_symbol: Some("000002.SZ".to_string()),
            successor_ratio: Some(0.5),
            successor_cash: Some(1.0),
        }],
    )
    .expect("dataset");

    let broker = BrokerSimulator::new_with_execution_price(
        ChinaAShareCostModel::default(),
        ChinaEquityRuleHooks::default(),
        PriceField::Open,
    );
    let mut engine = BacktestEngine::new(
        data,
        BuyThenHoldStrategy,
        broker,
        BacktestConfig {
            initial_cash: 100_000.0,
            benchmark_code: "000300.SH".to_string(),
            start_date: Some(date1),
            end_date: Some(date2),
            decision_lag_trading_days: 0,
            execution_price_field: PriceField::Open,
        },
    );

    let result = engine.run().expect("backtest succeeds");
    assert!(result.equity_curve.iter().any(|point| {
        point
            .notes
            .contains("successor_conversion 000001.SZ->000002.SZ")
    }));
    assert!(result.fills.iter().all(
        |fill| !fill.reason.contains("delisted_cash_settlement") || fill.symbol != "000001.SZ"
    ));
    let successor_holding = result
        .holdings_summary
        .iter()
        .find(|holding| holding.symbol == "000002.SZ")
        .expect("successor holding exists");
    assert_eq!(successor_holding.quantity, 500);
    assert!(
        result
            .holdings_summary
            .iter()
            .all(|holding| holding.symbol != "000001.SZ")
    );
    assert!(result.account_events.iter().any(|event| {
        event
            .note
            .contains("successor_conversion 000001.SZ->000002.SZ")
            && event.note.contains("cash=1000.00")
    }));
}
