use chrono::NaiveDate;
use fidc_core::{
    BenchmarkSnapshot, BrokerSimulator, CandidateEligibility, ChinaAShareCostModel,
    ChinaEquityRuleHooks, DailyFactorSnapshot, DailyMarketSnapshot, DataSet, Instrument,
    IntradayExecutionQuote, MatchingType, OrderIntent, OrderStatus, PortfolioState, PriceField,
    ProcessEventKind, 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_executes_order_shares_and_order_lots() {
    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::Shares {
                        symbol: "000002.SZ".to_string(),
                        quantity: 250,
                        reason: "shares_buy".to_string(),
                    },
                    OrderIntent::Lots {
                        symbol: "000002.SZ".to_string(),
                        lots: 3,
                        reason: "lots_buy".to_string(),
                    },
                ],
                notes: Vec::new(),
                diagnostics: Vec::new(),
            },
        )
        .expect("broker execution");

    assert_eq!(report.order_events.len(), 2);
    assert_eq!(report.fill_events.len(), 2);
    assert_eq!(report.fill_events[0].quantity, 200);
    assert_eq!(report.fill_events[1].quantity, 300);
    assert_eq!(
        portfolio.position("000002.SZ").expect("position").quantity,
        500
    );
}

#[test]
fn broker_executes_order_percent_and_target_percent() {
    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 broker = BrokerSimulator::new_with_execution_price(
        ChinaAShareCostModel::default(),
        ChinaEquityRuleHooks::default(),
        PriceField::Open,
    );

    let mut percent_portfolio = PortfolioState::new(1_000_000.0);
    let percent_report = broker
        .execute(
            date,
            &mut percent_portfolio,
            &data,
            &StrategyDecision {
                rebalance: false,
                target_weights: BTreeMap::new(),
                exit_symbols: BTreeSet::new(),
                order_intents: vec![OrderIntent::Percent {
                    symbol: "000002.SZ".to_string(),
                    percent: 0.10,
                    reason: "percent_buy".to_string(),
                }],
                notes: Vec::new(),
                diagnostics: Vec::new(),
            },
        )
        .expect("percent execution");
    assert_eq!(percent_report.fill_events.len(), 1);
    assert_eq!(percent_report.fill_events[0].quantity, 10_000);

    let mut target_percent_portfolio = PortfolioState::new(1_000_000.0);
    let target_percent_report = broker
        .execute(
            date,
            &mut target_percent_portfolio,
            &data,
            &StrategyDecision {
                rebalance: false,
                target_weights: BTreeMap::new(),
                exit_symbols: BTreeSet::new(),
                order_intents: vec![OrderIntent::TargetPercent {
                    symbol: "000002.SZ".to_string(),
                    target_percent: 0.0505,
                    reason: "target_percent_buy".to_string(),
                }],
                notes: Vec::new(),
                diagnostics: Vec::new(),
            },
        )
        .expect("target percent execution");
    assert_eq!(target_percent_report.fill_events.len(), 1);
    assert_eq!(target_percent_report.fill_events[0].quantity, 5_000);
}

#[test]
fn broker_uses_day_open_price_for_open_auction_matching() {
    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 09:25:00".to_string()),
            day_open: 9.8,
            open: 10.0,
            high: 10.1,
            low: 9.7,
            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("open_auction".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::DayOpen,
    )
    .with_slippage_model(SlippageModel::PriceRatio(0.05));

    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: 98_000.0,
                    reason: "open_auction_match".to_string(),
                }],
                notes: Vec::new(),
                diagnostics: Vec::new(),
            },
        )
        .expect("broker execution");

    assert_eq!(report.fill_events.len(), 1);
    assert!((report.fill_events[0].price - 9.8).abs() < 1e-9);
    let position = portfolio.position("000002.SZ").expect("position");
    let expected_total_equity = portfolio.cash() + position.quantity as f64 * 9.8;
    assert_eq!(report.account_events.len(), 1);
    assert!((report.account_events[0].total_equity - expected_total_equity).abs() < 1e-6);
}

#[test]
fn broker_open_auction_uses_auction_volume_without_quote_liquidity() {
    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 09:25:00".to_string()),
            day_open: 9.8,
            open: 10.0,
            high: 10.1,
            low: 9.7,
            close: 10.0,
            last_price: 10.0,
            bid1: 0.0,
            ask1: 0.0,
            prev_close: 10.0,
            volume: 1_000,
            tick_volume: 1_000,
            bid1_volume: 0,
            ask1_volume: 0,
            trading_phase: Some("open_auction".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::DayOpen,
    )
    .with_volume_percent(0.25);

    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: 9_800.0,
                    reason: "open_auction_volume".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, 200);
    assert_eq!(report.fill_events[0].price, 9.8);
}

#[test]
fn broker_cancels_buy_when_open_hits_upper_limit() {
    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 09:30:00".to_string()),
            day_open: 11.0,
            open: 11.0,
            high: 11.0,
            low: 10.8,
            close: 10.9,
            last_price: 11.0,
            bid1: 10.99,
            ask1: 11.0,
            prev_close: 10.0,
            volume: 100_000,
            tick_volume: 100_000,
            bid1_volume: 50_000,
            ask1_volume: 50_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: "upper_limit_buy".to_string(),
                }],
                notes: Vec::new(),
                diagnostics: Vec::new(),
            },
        )
        .expect("broker execution");

    assert!(report.fill_events.is_empty());
    assert_eq!(report.order_events.len(), 1);
    assert_eq!(
        report.order_events[0].status,
        fidc_core::OrderStatus::Canceled
    );
    assert!(
        report.order_events[0]
            .reason
            .contains("open at or above upper limit")
    );
    assert!(report.process_events.iter().any(|event| {
        event.kind == ProcessEventKind::OrderCreationReject
            && event.symbol.as_deref() == Some("000002.SZ")
            && event.side == Some(fidc_core::OrderSide::Buy)
    }));
}

#[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 broker_cancels_market_order_remainder_when_intraday_quote_liquidity_exhausted() {
    let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
    let data = DataSet::from_components_with_actions_and_quotes(
        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.2,
            low: 9.8,
            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,
        }],
        Vec::new(),
        vec![IntradayExecutionQuote {
            date,
            symbol: "000002.SZ".to_string(),
            timestamp: date.and_hms_opt(10, 18, 3).unwrap(),
            last_price: 10.02,
            bid1: 10.01,
            ask1: 10.03,
            bid1_volume: 2,
            ask1_volume: 2,
            volume_delta: 1,
            amount_delta: 0.0,
            trading_phase: Some("continuous".to_string()),
        }],
    )
    .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());

    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: 5_100.0,
                    reason: "intraday_quote_partial".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, 200);
    assert_eq!(report.order_events.len(), 1);
    assert_eq!(
        report.order_events[0].status,
        fidc_core::OrderStatus::Canceled
    );
    assert!(report.order_events[0].reason.contains(
        "partial fill due to intraday quote liquidity exhausted; remaining quantity canceled"
    ));
    assert!(
        report
            .diagnostics
            .iter()
            .any(|item| item.contains("order_remainder_canceled symbol=000002.SZ side=buy"))
    );
}

#[test]
fn broker_cancels_market_buy_when_tick_has_no_volume() {
    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.0,
            low: 10.0,
            close: 10.0,
            last_price: 10.0,
            bid1: 9.99,
            ask1: 10.01,
            prev_close: 10.0,
            volume: 0,
            tick_volume: 0,
            bid1_volume: 0,
            ask1_volume: 0,
            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: 10_000.0,
                    reason: "no_volume_buy".to_string(),
                }],
                notes: Vec::new(),
                diagnostics: Vec::new(),
            },
        )
        .expect("broker execution");

    assert_eq!(report.fill_events.len(), 0);
    assert_eq!(report.order_events.len(), 1);
    assert_eq!(
        report.order_events[0].status,
        fidc_core::OrderStatus::Canceled
    );
    assert!(report.order_events[0].reason.contains("tick no volume"));
}

#[test]
fn broker_splits_intraday_quote_fills_and_tracks_commission_by_order() {
    let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
    let data = DataSet::from_components_with_actions_and_quotes(
        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.2,
            low: 9.8,
            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,
        }],
        Vec::new(),
        vec![
            IntradayExecutionQuote {
                date,
                symbol: "000002.SZ".to_string(),
                timestamp: date.and_hms_opt(10, 18, 3).unwrap(),
                last_price: 10.01,
                bid1: 10.0,
                ask1: 10.02,
                bid1_volume: 1,
                ask1_volume: 1,
                volume_delta: 1,
                amount_delta: 0.0,
                trading_phase: Some("continuous".to_string()),
            },
            IntradayExecutionQuote {
                date,
                symbol: "000002.SZ".to_string(),
                timestamp: date.and_hms_opt(10, 18, 6).unwrap(),
                last_price: 10.03,
                bid1: 10.02,
                ask1: 10.04,
                bid1_volume: 1,
                ask1_volume: 1,
                volume_delta: 1,
                amount_delta: 0.0,
                trading_phase: Some("continuous".to_string()),
            },
        ],
    )
    .expect("dataset");
    let mut portfolio = PortfolioState::new(1_000_000.0);
    let broker = BrokerSimulator::new_with_execution_price(
        ChinaAShareCostModel::default(),
        ChinaEquityRuleHooks::default(),
        PriceField::Last,
    );

    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: 2_500.0,
                    reason: "intraday_split_fill".to_string(),
                }],
                notes: Vec::new(),
                diagnostics: Vec::new(),
            },
        )
        .expect("broker execution");

    assert_eq!(report.order_events.len(), 1);
    assert_eq!(
        report.order_events[0].status,
        fidc_core::OrderStatus::Filled
    );
    assert_eq!(report.fill_events.len(), 2);
    assert_eq!(report.fill_events[0].quantity, 100);
    assert_eq!(report.fill_events[1].quantity, 100);
    assert!((report.fill_events[0].price - 10.01).abs() < 1e-9);
    assert!((report.fill_events[1].price - 10.03).abs() < 1e-9);
    assert!((report.fill_events[0].commission - 5.0).abs() < 1e-9);
    assert_eq!(report.fill_events[1].commission, 0.0);
    assert_eq!(report.account_events.len(), 2);
    assert!(
        report
            .diagnostics
            .iter()
            .any(|item| item.contains("order_split_fill symbol=000002.SZ side=buy"))
    );
    assert_eq!(
        report
            .process_events
            .iter()
            .filter(|event| event.kind == ProcessEventKind::Trade)
            .count(),
        2
    );
    assert!(report.process_events.iter().any(|event| {
        event.kind == ProcessEventKind::OrderPendingNew
            && event.symbol.as_deref() == Some("000002.SZ")
    }));
    assert!(report.process_events.iter().any(|event| {
        event.kind == ProcessEventKind::OrderCreationPass
            && event.symbol.as_deref() == Some("000002.SZ")
    }));
}

#[test]
fn broker_aggregates_intraday_quote_fills_into_vwap_leg() {
    let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
    let data = DataSet::from_components_with_actions_and_quotes(
        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.2,
            low: 9.8,
            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,
        }],
        Vec::new(),
        vec![
            IntradayExecutionQuote {
                date,
                symbol: "000002.SZ".to_string(),
                timestamp: date.and_hms_opt(10, 18, 3).unwrap(),
                last_price: 10.01,
                bid1: 10.0,
                ask1: 10.02,
                bid1_volume: 1,
                ask1_volume: 1,
                volume_delta: 1,
                amount_delta: 0.0,
                trading_phase: Some("continuous".to_string()),
            },
            IntradayExecutionQuote {
                date,
                symbol: "000002.SZ".to_string(),
                timestamp: date.and_hms_opt(10, 18, 6).unwrap(),
                last_price: 10.03,
                bid1: 10.02,
                ask1: 10.04,
                bid1_volume: 1,
                ask1_volume: 1,
                volume_delta: 1,
                amount_delta: 0.0,
                trading_phase: Some("continuous".to_string()),
            },
        ],
    )
    .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_matching_type(MatchingType::Vwap);

    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: 2_500.0,
                    reason: "intraday_vwap_fill".to_string(),
                }],
                notes: Vec::new(),
                diagnostics: Vec::new(),
            },
        )
        .expect("broker execution");

    assert_eq!(report.order_events.len(), 1);
    assert_eq!(
        report.order_events[0].status,
        fidc_core::OrderStatus::Filled
    );
    assert_eq!(report.fill_events.len(), 1);
    assert_eq!(report.fill_events[0].quantity, 200);
    assert!((report.fill_events[0].price - 10.02).abs() < 1e-9);
    assert!((report.fill_events[0].commission - 5.0).abs() < 1e-9);
    assert_eq!(report.account_events.len(), 1);
    assert_eq!(
        report
            .process_events
            .iter()
            .filter(|event| event.kind == ProcessEventKind::Trade)
            .count(),
        1
    );
}

#[test]
fn broker_uses_best_own_price_for_intraday_matching() {
    let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
    let data = DataSet::from_components_with_actions_and_quotes(
        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.2,
            low: 9.8,
            close: 10.0,
            last_price: 10.0,
            bid1: 9.98,
            ask1: 10.02,
            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,
        }],
        Vec::new(),
        vec![IntradayExecutionQuote {
            date,
            symbol: "000002.SZ".to_string(),
            timestamp: date.and_hms_opt(10, 18, 3).unwrap(),
            last_price: 10.00,
            bid1: 9.98,
            ask1: 10.02,
            bid1_volume: 1,
            ask1_volume: 1,
            volume_delta: 1,
            amount_delta: 0.0,
            trading_phase: Some("continuous".to_string()),
        }],
    )
    .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_matching_type(MatchingType::NextTickBestOwn);

    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: 1_500.0,
                    reason: "best_own_fill".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, 100);
    assert!((report.fill_events[0].price - 9.98).abs() < 1e-9);
}

#[test]
fn broker_uses_best_counterparty_price_for_intraday_matching() {
    let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
    let data = DataSet::from_components_with_actions_and_quotes(
        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.2,
            low: 9.8,
            close: 10.0,
            last_price: 10.0,
            bid1: 9.98,
            ask1: 10.02,
            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,
        }],
        Vec::new(),
        vec![IntradayExecutionQuote {
            date,
            symbol: "000002.SZ".to_string(),
            timestamp: date.and_hms_opt(10, 18, 3).unwrap(),
            last_price: 10.00,
            bid1: 9.98,
            ask1: 10.02,
            bid1_volume: 1,
            ask1_volume: 1,
            volume_delta: 1,
            amount_delta: 0.0,
            trading_phase: Some("continuous".to_string()),
        }],
    )
    .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_matching_type(MatchingType::NextTickBestCounterparty);

    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: 1_500.0,
                    reason: "best_counterparty_fill".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, 100);
    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
    );
    assert!(
        report
            .diagnostics
            .iter()
            .any(|line| line.contains("rebalance_target_denied symbol=000001.SZ side=sell")),
        "expected locked position denial diagnostics, got {:?}",
        report.diagnostics
    );
    assert!(
        report
            .diagnostics
            .iter()
            .any(|line| line.contains("rebalance_buy_reduced symbol=000002.SZ")),
        "expected unfunded buy reduction diagnostics, got {:?}",
        report.diagnostics
    );
}

#[test]
fn rebalance_uses_prev_close_for_open_auction_valuation() {
    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 09:25: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: 10.0,
                volume: 100_000,
                tick_volume: 100_000,
                bid1_volume: 100_000,
                ask1_volume: 100_000,
                trading_phase: Some("open_auction".to_string()),
                paused: false,
                upper_limit: 22.0,
                lower_limit: 9.0,
                price_tick: 0.01,
            },
            DailyMarketSnapshot {
                date,
                symbol: "000002.SZ".to_string(),
                timestamp: Some("2024-01-10 09:25: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("open_auction".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: 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,
                symbol: "000002.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(),
            },
        ],
        vec![
            CandidateEligibility {
                date,
                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,
                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, 1_000, 10.0);

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

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

    let held = portfolio.position("000001.SZ").expect("held position");
    let target = portfolio.position("000002.SZ").expect("target position");
    assert_eq!(held.quantity, 500);
    assert_eq!(target.quantity, 400);
    assert_eq!(report.fill_events.len(), 2);
    assert!(
        report
            .fill_events
            .iter()
            .any(|fill| fill.symbol == "000001.SZ"
                && fill.side == fidc_core::OrderSide::Sell
                && fill.quantity == 500)
    );
    assert!(
        report
            .fill_events
            .iter()
            .any(|fill| fill.symbol == "000002.SZ"
                && fill.side == fidc_core::OrderSide::Buy
                && fill.quantity == 400)
    );
}

#[test]
fn rebalance_optimizer_prioritizes_higher_target_weight_when_cash_is_tight() {
    let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
    let data = DataSet::from_components(
        vec![
            Instrument {
                symbol: "000001.SZ".to_string(),
                name: "LowerWeight".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: "HigherWeight".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.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,
            },
            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: "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: true,
                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(10_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: true,
                target_weights: BTreeMap::from([
                    ("000001.SZ".to_string(), 0.2),
                    ("000002.SZ".to_string(), 0.8),
                ]),
                exit_symbols: BTreeSet::new(),
                order_intents: Vec::new(),
                notes: Vec::new(),
                diagnostics: Vec::new(),
            },
        )
        .expect("broker execution");

    let lower_weight_qty = portfolio
        .position("000001.SZ")
        .map(|position| position.quantity)
        .unwrap_or(0);
    let higher_weight_qty = portfolio
        .position("000002.SZ")
        .map(|position| position.quantity)
        .unwrap_or(0);
    assert!(
        higher_weight_qty > lower_weight_qty,
        "cash-constrained rebalance should preserve more exposure for the higher target weight, got low={} high={}",
        lower_weight_qty,
        higher_weight_qty
    );
    assert!(
        report
            .order_events
            .iter()
            .any(|event| event.symbol == "000002.SZ" && event.side == fidc_core::OrderSide::Buy)
    );
    assert!(
        report
            .diagnostics
            .iter()
            .any(|line| line.contains("rebalance_safety_scaled")
                || line.contains("rebalance_buy_reduced")),
        "expected rebalance diagnostics when cash is tight, got {:?}",
        report.diagnostics
    );
}

#[test]
fn broker_uses_board_specific_min_quantity_and_step_size_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: "board_min_qty".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, 1049);
}

#[test]
fn broker_allows_bjse_quantities_above_minimum_without_round_lot_step() {
    let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
    let data = DataSet::from_components(
        vec![Instrument {
            symbol: "430001.BJ".to_string(),
            name: "BJSE".to_string(),
            board: "BJS".to_string(),
            round_lot: 100,
            listed_at: None,
            delisted_at: None,
            status: "active".to_string(),
        }],
        vec![DailyMarketSnapshot {
            date,
            symbol: "430001.BJ".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: "430001.BJ".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: "430001.BJ".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_010.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: "430001.BJ".to_string(),
                    value: 1_010.0,
                    reason: "bj_min_qty".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, 100);
}

#[test]
fn broker_allows_full_odd_lot_sell_when_liquidating_position() {
    let prev_date = NaiveDate::from_ymd_opt(2024, 1, 9).unwrap();
    let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
    let symbol = "000001.SZ";
    let data = DataSet::from_components(
        vec![Instrument {
            symbol: symbol.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: 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: 10_000,
            tick_volume: 1_400,
            bid1_volume: 350,
            ask1_volume: 10_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: 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(),
        }],
        vec![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,
        }],
        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(symbol).buy(prev_date, 350, 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: false,
                target_weights: BTreeMap::new(),
                exit_symbols: BTreeSet::new(),
                order_intents: vec![OrderIntent::TargetValue {
                    symbol: symbol.to_string(),
                    target_value: 0.0,
                    reason: "odd_lot_liquidation".to_string(),
                }],
                notes: Vec::new(),
                diagnostics: Vec::new(),
            },
        )
        .expect("broker execution");

    assert_eq!(report.fill_events.len(), 1);
    assert_eq!(report.fill_events[0].side, fidc_core::OrderSide::Sell);
    assert_eq!(report.fill_events[0].quantity, 350);
    assert!(portfolio.position(symbol).is_none());
}

#[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(),
        ]
    );
}

fn two_day_limit_order_data(day1_open: f64, day2_open: f64) -> DataSet {
    let day1 = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
    let day2 = NaiveDate::from_ymd_opt(2024, 1, 11).unwrap();
    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: day1,
                symbol: "000002.SZ".to_string(),
                timestamp: Some("2024-01-10 09:30:00".to_string()),
                day_open: day1_open,
                open: day1_open,
                high: day1_open + 0.2,
                low: day1_open - 0.2,
                close: day1_open,
                last_price: day1_open,
                bid1: day1_open - 0.01,
                ask1: day1_open + 0.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,
            },
            DailyMarketSnapshot {
                date: day2,
                symbol: "000002.SZ".to_string(),
                timestamp: Some("2024-01-11 09:30:00".to_string()),
                day_open: day2_open,
                open: day2_open,
                high: day2_open + 0.2,
                low: day2_open - 0.2,
                close: day2_open,
                last_price: day2_open,
                bid1: day2_open - 0.01,
                ask1: day2_open + 0.01,
                prev_close: day1_open,
                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: day1,
                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(),
            },
            DailyFactorSnapshot {
                date: day2,
                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: day1,
                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: day2,
                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: day1,
                benchmark: "000300.SH".to_string(),
                open: 100.0,
                close: 100.0,
                prev_close: 99.0,
                volume: 1_000_000,
            },
            BenchmarkSnapshot {
                date: day2,
                benchmark: "000300.SH".to_string(),
                open: 100.0,
                close: 100.0,
                prev_close: 100.0,
                volume: 1_000_000,
            },
        ],
    )
    .expect("dataset")
}

#[test]
fn broker_rejects_open_limit_buy_at_market_close() {
    let day1 = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
    let day2 = NaiveDate::from_ymd_opt(2024, 1, 11).unwrap();
    let data = two_day_limit_order_data(10.0, 9.7);
    let broker = BrokerSimulator::new_with_execution_price(
        ChinaAShareCostModel::default(),
        ChinaEquityRuleHooks::default(),
        PriceField::Open,
    );
    let mut portfolio = PortfolioState::new(1_000_000.0);

    let day1_report = broker
        .execute(
            day1,
            &mut portfolio,
            &data,
            &StrategyDecision {
                rebalance: false,
                target_weights: BTreeMap::new(),
                exit_symbols: BTreeSet::new(),
                order_intents: vec![OrderIntent::LimitShares {
                    symbol: "000002.SZ".to_string(),
                    quantity: 200,
                    limit_price: 9.8,
                    reason: "limit_entry".to_string(),
                }],
                notes: Vec::new(),
                diagnostics: Vec::new(),
            },
        )
        .expect("day1 execution");
    assert_eq!(day1_report.fill_events.len(), 0);
    assert_eq!(day1_report.order_events.len(), 1);
    assert_eq!(day1_report.order_events[0].status, OrderStatus::Pending);
    let order_id = day1_report.order_events[0].order_id.expect("order id");

    let close_report = broker.after_trading(day1);
    assert!(close_report.fill_events.is_empty());
    assert_eq!(close_report.order_events.len(), 1);
    assert_eq!(close_report.order_events[0].order_id, Some(order_id));
    assert_eq!(close_report.order_events[0].status, OrderStatus::Rejected);
    assert!(
        close_report.order_events[0]
            .reason
            .contains("Order Rejected: 000002.SZ can not match. Market close.")
    );
    assert!(close_report.process_events.iter().any(|event| {
        event.kind == ProcessEventKind::OrderUnsolicitedUpdate && event.order_id == Some(order_id)
    }));

    let day2_report = broker
        .execute(
            day2,
            &mut portfolio,
            &data,
            &StrategyDecision {
                rebalance: false,
                target_weights: BTreeMap::new(),
                exit_symbols: BTreeSet::new(),
                order_intents: Vec::new(),
                notes: Vec::new(),
                diagnostics: Vec::new(),
            },
        )
        .expect("day2 execution");
    assert!(day2_report.fill_events.is_empty());
    assert!(day2_report.order_events.is_empty());
    assert!(portfolio.position("000002.SZ").is_none());
}

#[test]
fn broker_uses_limit_price_slippage_for_limit_orders() {
    let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
    let data = two_day_limit_order_data(10.0, 10.2);
    let broker = BrokerSimulator::new_with_execution_price(
        ChinaAShareCostModel::default(),
        ChinaEquityRuleHooks::default(),
        PriceField::Open,
    )
    .with_slippage_model(SlippageModel::LimitPrice);
    let mut portfolio = PortfolioState::new(1_000_000.0);

    let report = broker
        .execute(
            date,
            &mut portfolio,
            &data,
            &StrategyDecision {
                rebalance: false,
                target_weights: BTreeMap::new(),
                exit_symbols: BTreeSet::new(),
                order_intents: vec![OrderIntent::LimitShares {
                    symbol: "000002.SZ".to_string(),
                    quantity: 200,
                    limit_price: 10.1,
                    reason: "limit_entry".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_executes_limit_value_and_limit_percent_intents() {
    let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
    let data = two_day_limit_order_data(10.0, 10.0);
    let broker = BrokerSimulator::new_with_execution_price(
        ChinaAShareCostModel::default(),
        ChinaEquityRuleHooks::default(),
        PriceField::Open,
    );

    let mut value_portfolio = PortfolioState::new(1_000_000.0);
    let value_report = broker
        .execute(
            date,
            &mut value_portfolio,
            &data,
            &StrategyDecision {
                rebalance: false,
                target_weights: BTreeMap::new(),
                exit_symbols: BTreeSet::new(),
                order_intents: vec![OrderIntent::LimitValue {
                    symbol: "000002.SZ".to_string(),
                    value: 20_000.0,
                    limit_price: 10.1,
                    reason: "limit_value".to_string(),
                }],
                notes: Vec::new(),
                diagnostics: Vec::new(),
            },
        )
        .expect("broker execution");
    assert_eq!(value_report.fill_events.len(), 1);
    assert!(value_portfolio.position("000002.SZ").is_some());

    let mut percent_portfolio = PortfolioState::new(1_000_000.0);
    let percent_report = broker
        .execute(
            date,
            &mut percent_portfolio,
            &data,
            &StrategyDecision {
                rebalance: false,
                target_weights: BTreeMap::new(),
                exit_symbols: BTreeSet::new(),
                order_intents: vec![OrderIntent::LimitPercent {
                    symbol: "000002.SZ".to_string(),
                    percent: 0.02,
                    limit_price: 10.1,
                    reason: "limit_percent".to_string(),
                }],
                notes: Vec::new(),
                diagnostics: Vec::new(),
            },
        )
        .expect("broker execution");
    assert_eq!(percent_report.fill_events.len(), 1);
    assert!(percent_portfolio.position("000002.SZ").is_some());
}

#[test]
fn broker_cancels_open_order_by_order_id() {
    let day1 = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
    let data = two_day_limit_order_data(10.0, 10.1);
    let broker = BrokerSimulator::new_with_execution_price(
        ChinaAShareCostModel::default(),
        ChinaEquityRuleHooks::default(),
        PriceField::Open,
    );
    let mut portfolio = PortfolioState::new(1_000_000.0);

    let day1_report = broker
        .execute(
            day1,
            &mut portfolio,
            &data,
            &StrategyDecision {
                rebalance: false,
                target_weights: BTreeMap::new(),
                exit_symbols: BTreeSet::new(),
                order_intents: vec![OrderIntent::LimitShares {
                    symbol: "000002.SZ".to_string(),
                    quantity: 200,
                    limit_price: 9.8,
                    reason: "limit_entry".to_string(),
                }],
                notes: Vec::new(),
                diagnostics: Vec::new(),
            },
        )
        .expect("day1 execution");
    let order_id = day1_report.order_events[0].order_id.expect("order id");

    let day2_report = broker
        .execute(
            day1,
            &mut portfolio,
            &data,
            &StrategyDecision {
                rebalance: false,
                target_weights: BTreeMap::new(),
                exit_symbols: BTreeSet::new(),
                order_intents: vec![OrderIntent::CancelOrder {
                    order_id,
                    reason: "user_cancel".to_string(),
                }],
                notes: Vec::new(),
                diagnostics: Vec::new(),
            },
        )
        .expect("day2 execution");

    assert!(day2_report.fill_events.is_empty());
    assert!(
        day2_report
            .order_events
            .iter()
            .any(|event| event.order_id == Some(order_id) && event.status == OrderStatus::Canceled)
    );
    assert!(day2_report.process_events.iter().any(|event| {
        event.kind == ProcessEventKind::OrderPendingCancel && event.order_id == Some(order_id)
    }));
    assert!(day2_report.process_events.iter().any(|event| {
        event.kind == ProcessEventKind::OrderCancellationPass && event.order_id == Some(order_id)
    }));
    assert!(portfolio.position("000002.SZ").is_none());
}

#[test]
fn broker_emits_cancellation_reject_for_unknown_order() {
    let day1 = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
    let data = two_day_limit_order_data(10.0, 10.1);
    let broker = BrokerSimulator::new_with_execution_price(
        ChinaAShareCostModel::default(),
        ChinaEquityRuleHooks::default(),
        PriceField::Open,
    );
    let mut portfolio = PortfolioState::new(1_000_000.0);

    let report = broker
        .execute(
            day1,
            &mut portfolio,
            &data,
            &StrategyDecision {
                rebalance: false,
                target_weights: BTreeMap::new(),
                exit_symbols: BTreeSet::new(),
                order_intents: vec![OrderIntent::CancelOrder {
                    order_id: 999_999,
                    reason: "user_cancel".to_string(),
                }],
                notes: Vec::new(),
                diagnostics: Vec::new(),
            },
        )
        .expect("cancel reject execution");

    assert!(report.order_events.is_empty());
    assert!(report.fill_events.is_empty());
    assert!(report.process_events.iter().any(|event| {
        event.kind == ProcessEventKind::OrderCancellationReject && event.order_id == Some(999_999)
    }));
}

#[test]
fn broker_reserves_sellable_quantity_for_open_limit_sells() {
    let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
    let prev_date = NaiveDate::from_ymd_opt(2024, 1, 9).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 09:30:00".to_string()),
            day_open: 10.0,
            open: 10.0,
            high: 10.2,
            low: 9.8,
            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 broker = BrokerSimulator::new_with_execution_price(
        ChinaAShareCostModel::default(),
        ChinaEquityRuleHooks::default(),
        PriceField::Open,
    );
    let mut portfolio = PortfolioState::new(1_000_000.0);
    portfolio
        .position_mut("000002.SZ")
        .buy(prev_date, 300, 10.0);

    let report = broker
        .execute(
            date,
            &mut portfolio,
            &data,
            &StrategyDecision {
                rebalance: false,
                target_weights: BTreeMap::new(),
                exit_symbols: BTreeSet::new(),
                order_intents: vec![
                    OrderIntent::LimitShares {
                        symbol: "000002.SZ".to_string(),
                        quantity: -200,
                        limit_price: 10.5,
                        reason: "reserve_sell".to_string(),
                    },
                    OrderIntent::Shares {
                        symbol: "000002.SZ".to_string(),
                        quantity: -200,
                        reason: "second_sell".to_string(),
                    },
                ],
                notes: Vec::new(),
                diagnostics: Vec::new(),
            },
        )
        .expect("broker execution");

    assert_eq!(report.order_events.len(), 2);
    assert_eq!(report.order_events[0].status, OrderStatus::Pending);
    assert_eq!(report.order_events[1].status, OrderStatus::PartiallyFilled);
    assert_eq!(report.order_events[1].filled_quantity, 100);
    assert_eq!(
        portfolio.position("000002.SZ").expect("position").quantity,
        200
    );
}
