use chrono::NaiveDate;
use fidc_core::{
    BenchmarkSnapshot, BrokerSimulator, CandidateEligibility, ChinaAShareCostModel,
    ChinaEquityRuleHooks, DailyFactorSnapshot, DailyMarketSnapshot, DataSet, Instrument,
    OrderIntent, PortfolioState, PriceField, SlippageModel, StrategyDecision,
};
use std::collections::{BTreeMap, BTreeSet};

#[test]
fn broker_executes_explicit_order_value_buy() {
    let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
    let data = DataSet::from_components(
        vec![Instrument {
            symbol: "000002.SZ".to_string(),
            name: "Test".to_string(),
            board: "SZ".to_string(),
            round_lot: 100,
            listed_at: None,
            delisted_at: None,
            status: "active".to_string(),
        }],
        vec![DailyMarketSnapshot {
            date,
            symbol: "000002.SZ".to_string(),
            timestamp: Some("2024-01-10 10:18:00".to_string()),
            day_open: 10.0,
            open: 10.0,
            high: 10.1,
            low: 9.9,
            close: 10.0,
            last_price: 10.0,
            bid1: 9.99,
            ask1: 10.01,
            prev_close: 10.0,
            volume: 100_000,
            tick_volume: 100_000,
            bid1_volume: 80_000,
            ask1_volume: 80_000,
            trading_phase: Some("continuous".to_string()),
            paused: false,
            upper_limit: 11.0,
            lower_limit: 9.0,
            price_tick: 0.01,
        }],
        vec![DailyFactorSnapshot {
            date,
            symbol: "000002.SZ".to_string(),
            market_cap_bn: 50.0,
            free_float_cap_bn: 45.0,
            pe_ttm: 15.0,
            turnover_ratio: Some(2.0),
            effective_turnover_ratio: Some(1.8),
            extra_factors: BTreeMap::new(),
        }],
        vec![CandidateEligibility {
            date,
            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,
            benchmark: "000300.SH".to_string(),
            open: 100.0,
            close: 100.0,
            prev_close: 99.0,
            volume: 1_000_000,
        }],
    )
    .expect("dataset");
    let mut portfolio = PortfolioState::new(1_000_000.0);
    let broker = BrokerSimulator::new_with_execution_price(
        ChinaAShareCostModel::default(),
        ChinaEquityRuleHooks::default(),
        PriceField::Open,
    );

    let report = broker
        .execute(
            date,
            &mut portfolio,
            &data,
            &StrategyDecision {
                rebalance: false,
                target_weights: BTreeMap::new(),
                exit_symbols: BTreeSet::new(),
                order_intents: vec![OrderIntent::Value {
                    symbol: "000002.SZ".to_string(),
                    value: 100_000.0,
                    reason: "test_order_value".to_string(),
                }],
                notes: Vec::new(),
                diagnostics: Vec::new(),
            },
        )
        .expect("broker execution");

    assert!(!report.fill_events.is_empty());
    assert_eq!(report.fill_events[0].symbol, "000002.SZ");
    assert_eq!(report.fill_events[0].side, fidc_core::OrderSide::Buy);
    assert!(portfolio.position("000002.SZ").is_some());
    assert!(portfolio.cash() < 1_000_000.0);
}

#[test]
fn broker_applies_price_ratio_slippage_on_snapshot_fills() {
    let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
    let data = DataSet::from_components(
        vec![Instrument {
            symbol: "000002.SZ".to_string(),
            name: "Test".to_string(),
            board: "SZ".to_string(),
            round_lot: 100,
            listed_at: None,
            delisted_at: None,
            status: "active".to_string(),
        }],
        vec![DailyMarketSnapshot {
            date,
            symbol: "000002.SZ".to_string(),
            timestamp: Some("2024-01-10 10:18:00".to_string()),
            day_open: 10.0,
            open: 10.0,
            high: 10.1,
            low: 9.9,
            close: 10.0,
            last_price: 10.0,
            bid1: 9.99,
            ask1: 10.01,
            prev_close: 10.0,
            volume: 100_000,
            tick_volume: 100_000,
            bid1_volume: 80_000,
            ask1_volume: 80_000,
            trading_phase: Some("continuous".to_string()),
            paused: false,
            upper_limit: 11.0,
            lower_limit: 9.0,
            price_tick: 0.01,
        }],
        vec![DailyFactorSnapshot {
            date,
            symbol: "000002.SZ".to_string(),
            market_cap_bn: 50.0,
            free_float_cap_bn: 45.0,
            pe_ttm: 15.0,
            turnover_ratio: Some(2.0),
            effective_turnover_ratio: Some(1.8),
            extra_factors: BTreeMap::new(),
        }],
        vec![CandidateEligibility {
            date,
            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,
            benchmark: "000300.SH".to_string(),
            open: 100.0,
            close: 100.0,
            prev_close: 99.0,
            volume: 1_000_000,
        }],
    )
    .expect("dataset");
    let mut portfolio = PortfolioState::new(1_000_000.0);
    let broker = BrokerSimulator::new_with_execution_price(
        ChinaAShareCostModel::default(),
        ChinaEquityRuleHooks::default(),
        PriceField::Open,
    )
    .with_slippage_model(SlippageModel::PriceRatio(0.01));

    let report = broker
        .execute(
            date,
            &mut portfolio,
            &data,
            &StrategyDecision {
                rebalance: false,
                target_weights: BTreeMap::new(),
                exit_symbols: BTreeSet::new(),
                order_intents: vec![OrderIntent::Value {
                    symbol: "000002.SZ".to_string(),
                    value: 100_000.0,
                    reason: "price_ratio_slippage".to_string(),
                }],
                notes: Vec::new(),
                diagnostics: Vec::new(),
            },
        )
        .expect("broker execution");

    assert_eq!(report.fill_events.len(), 1);
    assert!((report.fill_events[0].price - 10.1).abs() < 1e-9);
}

#[test]
fn broker_applies_tick_size_slippage_on_intraday_last_fills() {
    let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
    let data = DataSet::from_components(
        vec![Instrument {
            symbol: "000002.SZ".to_string(),
            name: "Test".to_string(),
            board: "SZ".to_string(),
            round_lot: 100,
            listed_at: None,
            delisted_at: None,
            status: "active".to_string(),
        }],
        vec![DailyMarketSnapshot {
            date,
            symbol: "000002.SZ".to_string(),
            timestamp: Some("2024-01-10 10:18:00".to_string()),
            day_open: 10.0,
            open: 10.0,
            high: 10.1,
            low: 9.9,
            close: 10.0,
            last_price: 10.0,
            bid1: 9.99,
            ask1: 10.01,
            prev_close: 10.0,
            volume: 100_000,
            tick_volume: 100_000,
            bid1_volume: 80_000,
            ask1_volume: 80_000,
            trading_phase: Some("continuous".to_string()),
            paused: false,
            upper_limit: 11.0,
            lower_limit: 9.0,
            price_tick: 0.01,
        }],
        vec![DailyFactorSnapshot {
            date,
            symbol: "000002.SZ".to_string(),
            market_cap_bn: 50.0,
            free_float_cap_bn: 45.0,
            pe_ttm: 15.0,
            turnover_ratio: Some(2.0),
            effective_turnover_ratio: Some(1.8),
            extra_factors: BTreeMap::new(),
        }],
        vec![CandidateEligibility {
            date,
            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,
            benchmark: "000300.SH".to_string(),
            open: 100.0,
            close: 100.0,
            prev_close: 99.0,
            volume: 1_000_000,
        }],
    )
    .expect("dataset");
    let mut portfolio = PortfolioState::new(1_000_000.0);
    let broker = BrokerSimulator::new_with_execution_price(
        ChinaAShareCostModel::default(),
        ChinaEquityRuleHooks::default(),
        PriceField::Last,
    )
    .with_intraday_execution_start_time(chrono::NaiveTime::from_hms_opt(10, 18, 0).unwrap())
    .with_slippage_model(SlippageModel::TickSize(2.0));

    let report = broker
        .execute(
            date,
            &mut portfolio,
            &data,
            &StrategyDecision {
                rebalance: false,
                target_weights: BTreeMap::new(),
                exit_symbols: BTreeSet::new(),
                order_intents: vec![OrderIntent::Value {
                    symbol: "000002.SZ".to_string(),
                    value: 100_000.0,
                    reason: "tick_slippage".to_string(),
                }],
                notes: Vec::new(),
                diagnostics: Vec::new(),
            },
        )
        .expect("broker execution");

    assert_eq!(report.fill_events.len(), 1);
    assert!((report.fill_events[0].price - 10.02).abs() < 1e-9);
}

#[test]
fn rebalance_optimizer_skips_unfunded_buy_when_existing_position_cannot_sell() {
    let prev_date = NaiveDate::from_ymd_opt(2024, 1, 9).unwrap();
    let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
    let data = DataSet::from_components(
        vec![
            Instrument {
                symbol: "000001.SZ".to_string(),
                name: "Held".to_string(),
                board: "SZ".to_string(),
                round_lot: 100,
                listed_at: None,
                delisted_at: None,
                status: "active".to_string(),
            },
            Instrument {
                symbol: "000002.SZ".to_string(),
                name: "Target".to_string(),
                board: "SZ".to_string(),
                round_lot: 100,
                listed_at: None,
                delisted_at: None,
                status: "active".to_string(),
            },
        ],
        vec![
            DailyMarketSnapshot {
                date,
                symbol: "000001.SZ".to_string(),
                timestamp: Some("2024-01-10 10:18:00".to_string()),
                day_open: 10.0,
                open: 10.0,
                high: 10.0,
                low: 9.5,
                close: 9.8,
                last_price: 9.8,
                bid1: 9.79,
                ask1: 9.81,
                prev_close: 10.0,
                volume: 100_000,
                tick_volume: 100_000,
                bid1_volume: 80_000,
                ask1_volume: 80_000,
                trading_phase: Some("continuous".to_string()),
                paused: false,
                upper_limit: 11.0,
                lower_limit: 9.0,
                price_tick: 0.01,
            },
            DailyMarketSnapshot {
                date,
                symbol: "000002.SZ".to_string(),
                timestamp: Some("2024-01-10 10:18:00".to_string()),
                day_open: 10.0,
                open: 10.0,
                high: 10.2,
                low: 9.8,
                close: 10.1,
                last_price: 10.1,
                bid1: 10.09,
                ask1: 10.11,
                prev_close: 10.0,
                volume: 100_000,
                tick_volume: 100_000,
                bid1_volume: 80_000,
                ask1_volume: 80_000,
                trading_phase: Some("continuous".to_string()),
                paused: false,
                upper_limit: 11.0,
                lower_limit: 9.0,
                price_tick: 0.01,
            },
        ],
        vec![
            DailyFactorSnapshot {
                date,
                symbol: "000001.SZ".to_string(),
                market_cap_bn: 50.0,
                free_float_cap_bn: 45.0,
                pe_ttm: 15.0,
                turnover_ratio: Some(2.0),
                effective_turnover_ratio: Some(1.8),
                extra_factors: BTreeMap::new(),
            },
            DailyFactorSnapshot {
                date,
                symbol: "000002.SZ".to_string(),
                market_cap_bn: 60.0,
                free_float_cap_bn: 50.0,
                pe_ttm: 18.0,
                turnover_ratio: Some(2.0),
                effective_turnover_ratio: Some(1.8),
                extra_factors: BTreeMap::new(),
            },
        ],
        vec![
            CandidateEligibility {
                date,
                symbol: "000001.SZ".to_string(),
                is_st: false,
                is_new_listing: false,
                is_paused: false,
                allow_buy: true,
                allow_sell: false,
                is_kcb: false,
                is_one_yuan: false,
            },
            CandidateEligibility {
                date,
                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,
            benchmark: "000300.SH".to_string(),
            open: 100.0,
            close: 100.0,
            prev_close: 99.0,
            volume: 1_000_000,
        }],
    )
    .expect("dataset");
    let mut portfolio = PortfolioState::new(0.0);
    portfolio
        .position_mut("000001.SZ")
        .buy(prev_date, 10_000, 10.0);
    let broker = BrokerSimulator::new_with_execution_price(
        ChinaAShareCostModel::default(),
        ChinaEquityRuleHooks::default(),
        PriceField::Open,
    );

    let report = broker
        .execute(
            date,
            &mut portfolio,
            &data,
            &StrategyDecision {
                rebalance: true,
                target_weights: BTreeMap::from([("000002.SZ".to_string(), 1.0)]),
                exit_symbols: BTreeSet::new(),
                order_intents: Vec::new(),
                notes: Vec::new(),
                diagnostics: Vec::new(),
            },
        )
        .expect("broker execution");

    assert!(
        report
            .order_events
            .iter()
            .all(|event| !(event.symbol == "000002.SZ" && event.side == fidc_core::OrderSide::Buy)),
        "optimizer should skip unfunded rebalance buy when locked holding cannot be sold"
    );
    assert_eq!(
        portfolio
            .position("000001.SZ")
            .map(|position| position.quantity)
            .unwrap_or(0),
        10_000
    );
}

#[test]
fn broker_uses_instrument_round_lot_for_buy_sizing() {
    let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
    let data = DataSet::from_components(
        vec![Instrument {
            symbol: "688001.SH".to_string(),
            name: "KSH".to_string(),
            board: "KSH".to_string(),
            round_lot: 200,
            listed_at: None,
            delisted_at: None,
            status: "active".to_string(),
        }],
        vec![DailyMarketSnapshot {
            date,
            symbol: "688001.SH".to_string(),
            timestamp: Some("2024-01-10 10:18:00".to_string()),
            day_open: 10.0,
            open: 10.0,
            high: 10.1,
            low: 9.9,
            close: 10.0,
            last_price: 10.0,
            bid1: 9.99,
            ask1: 10.01,
            prev_close: 10.0,
            volume: 100_000,
            tick_volume: 100_000,
            bid1_volume: 80_000,
            ask1_volume: 80_000,
            trading_phase: Some("continuous".to_string()),
            paused: false,
            upper_limit: 11.0,
            lower_limit: 9.0,
            price_tick: 0.01,
        }],
        vec![DailyFactorSnapshot {
            date,
            symbol: "688001.SH".to_string(),
            market_cap_bn: 50.0,
            free_float_cap_bn: 45.0,
            pe_ttm: 20.0,
            turnover_ratio: Some(2.0),
            effective_turnover_ratio: Some(1.8),
            extra_factors: BTreeMap::new(),
        }],
        vec![CandidateEligibility {
            date,
            symbol: "688001.SH".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,
            benchmark: "000300.SH".to_string(),
            open: 100.0,
            close: 100.0,
            prev_close: 99.0,
            volume: 1_000_000,
        }],
    )
    .expect("dataset");
    let mut portfolio = PortfolioState::new(10_500.0);
    let broker = BrokerSimulator::new_with_execution_price(
        ChinaAShareCostModel::default(),
        ChinaEquityRuleHooks::default(),
        PriceField::Open,
    );

    let report = broker
        .execute(
            date,
            &mut portfolio,
            &data,
            &StrategyDecision {
                rebalance: false,
                target_weights: BTreeMap::new(),
                exit_symbols: BTreeSet::new(),
                order_intents: vec![OrderIntent::Value {
                    symbol: "688001.SH".to_string(),
                    value: 10_500.0,
                    reason: "round_lot".to_string(),
                }],
                notes: Vec::new(),
                diagnostics: Vec::new(),
            },
        )
        .expect("broker execution");

    assert_eq!(report.fill_events.len(), 1);
    assert_eq!(report.fill_events[0].quantity, 1000);
}

#[test]
fn same_day_sell_then_rebuy_reinserts_position_at_end() {
    let prev_date = NaiveDate::from_ymd_opt(2024, 1, 9).unwrap();
    let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
    let symbols = ["000001.SZ", "000002.SZ", "000003.SZ"];
    let instruments = symbols
        .iter()
        .map(|symbol| Instrument {
            symbol: (*symbol).to_string(),
            name: (*symbol).to_string(),
            board: "SZ".to_string(),
            round_lot: 100,
            listed_at: None,
            delisted_at: None,
            status: "active".to_string(),
        })
        .collect::<Vec<_>>();
    let market = symbols
        .iter()
        .map(|symbol| DailyMarketSnapshot {
            date,
            symbol: (*symbol).to_string(),
            timestamp: Some("2024-01-10 10:18:00".to_string()),
            day_open: 10.0,
            open: 10.0,
            high: 10.1,
            low: 9.9,
            close: 10.0,
            last_price: 10.0,
            bid1: 9.99,
            ask1: 10.01,
            prev_close: 10.0,
            volume: 100_000,
            tick_volume: 100_000,
            bid1_volume: 80_000,
            ask1_volume: 80_000,
            trading_phase: Some("continuous".to_string()),
            paused: false,
            upper_limit: 11.0,
            lower_limit: 9.0,
            price_tick: 0.01,
        })
        .collect::<Vec<_>>();
    let factors = symbols
        .iter()
        .map(|symbol| DailyFactorSnapshot {
            date,
            symbol: (*symbol).to_string(),
            market_cap_bn: 50.0,
            free_float_cap_bn: 45.0,
            pe_ttm: 15.0,
            turnover_ratio: Some(2.0),
            effective_turnover_ratio: Some(1.8),
            extra_factors: BTreeMap::new(),
        })
        .collect::<Vec<_>>();
    let candidates = symbols
        .iter()
        .map(|symbol| CandidateEligibility {
            date,
            symbol: (*symbol).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,
        })
        .collect::<Vec<_>>();
    let data = DataSet::from_components(
        instruments,
        market,
        factors,
        candidates,
        vec![BenchmarkSnapshot {
            date,
            benchmark: "000300.SH".to_string(),
            open: 100.0,
            close: 100.0,
            prev_close: 99.0,
            volume: 1_000_000,
        }],
    )
    .expect("dataset");

    let mut portfolio = PortfolioState::new(1_000_000.0);
    portfolio
        .position_mut("000001.SZ")
        .buy(prev_date, 100, 10.0);
    portfolio
        .position_mut("000002.SZ")
        .buy(prev_date, 100, 10.0);
    portfolio
        .position_mut("000003.SZ")
        .buy(prev_date, 100, 10.0);

    let broker = BrokerSimulator::new_with_execution_price(
        ChinaAShareCostModel::default(),
        ChinaEquityRuleHooks::default(),
        PriceField::Open,
    );

    broker
        .execute(
            date,
            &mut portfolio,
            &data,
            &StrategyDecision {
                rebalance: false,
                target_weights: BTreeMap::new(),
                exit_symbols: BTreeSet::new(),
                order_intents: vec![
                    OrderIntent::TargetValue {
                        symbol: "000002.SZ".to_string(),
                        target_value: 0.0,
                        reason: "sell_then_rebuy".to_string(),
                    },
                    OrderIntent::Value {
                        symbol: "000002.SZ".to_string(),
                        value: 10_000.0,
                        reason: "sell_then_rebuy".to_string(),
                    },
                ],
                notes: Vec::new(),
                diagnostics: Vec::new(),
            },
        )
        .expect("broker execution");

    let symbols = portfolio.positions().keys().cloned().collect::<Vec<_>>();
    assert_eq!(
        symbols,
        vec![
            "000001.SZ".to_string(),
            "000003.SZ".to_string(),
            "000002.SZ".to_string(),
        ]
    );
}
