use chrono::NaiveDate;
use std::collections::{BTreeMap, BTreeSet};

use fidc_core::{
    BacktestConfig, BacktestEngine, BenchmarkSnapshot, BrokerSimulator, CandidateEligibility,
    CashReceivable, ChinaAShareCostModel, ChinaEquityRuleHooks, CorporateAction,
    DailyFactorSnapshot, DailyMarketSnapshot, DataSet, Instrument, PortfolioState, Position,
    PriceField, Strategy, StrategyContext, StrategyDecision,
};

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

#[test]
fn cash_dividend_adjusts_cost_basis_and_returns_cash_delta() {
    let mut position = Position::new("000001.SZ");
    position.buy(d(2025, 1, 2), 1_000, 10.0);

    let cash_delta = position.apply_cash_dividend(0.5);

    assert!((cash_delta - 500.0).abs() < 1e-9);
    assert_eq!(position.quantity, 1_000);
    assert!((position.average_cost - 9.5).abs() < 1e-9);
    assert!((position.last_price - 9.5).abs() < 1e-9);
}

#[test]
fn stock_split_scales_lots_quantity_and_average_cost() {
    let mut position = Position::new("000001.SZ");
    position.buy(d(2025, 1, 2), 1_000, 10.0);

    let delta_quantity = position.apply_split_ratio(1.2);

    assert_eq!(delta_quantity, 200);
    assert_eq!(position.quantity, 1_200);
    assert!((position.average_cost - (10.0 / 1.2)).abs() < 1e-9);
    assert!((position.last_price - (10.0 / 1.2)).abs() < 1e-9);
    assert_eq!(position.sellable_qty(d(2025, 1, 3)), 1_200);
}

#[test]
fn portfolio_settles_cash_receivable_on_payable_date() {
    let mut portfolio = PortfolioState::new(1_000_000.0);
    portfolio.add_cash_receivable(CashReceivable {
        symbol: "000001.SZ".to_string(),
        ex_date: d(2025, 1, 2),
        payable_date: d(2025, 1, 5),
        amount: 500.0,
        reason: "cash_dividend 0.5".to_string(),
    });

    let settled_early = portfolio.settle_cash_receivables(d(2025, 1, 4));
    assert!(settled_early.is_empty());
    assert!((portfolio.cash() - 1_000_000.0).abs() < 1e-9);

    let settled = portfolio.settle_cash_receivables(d(2025, 1, 5));
    assert_eq!(settled.len(), 1);
    assert!((portfolio.cash() - 1_000_500.0).abs() < 1e-9);
    assert!(portfolio.cash_receivables().is_empty());
}

struct BuyAndHoldStrategy {
    first_date: NaiveDate,
}

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

    fn on_day(
        &mut self,
        ctx: &StrategyContext<'_>,
    ) -> Result<StrategyDecision, fidc_core::BacktestError> {
        Ok(StrategyDecision {
            rebalance: false,
            target_weights: BTreeMap::new(),
            exit_symbols: BTreeSet::new(),
            order_intents: if ctx.execution_date == self.first_date {
                vec![fidc_core::OrderIntent::Shares {
                    symbol: "000001.SZ".to_string(),
                    quantity: 1_000,
                    reason: "initial_buy".to_string(),
                }]
            } else {
                Vec::new()
            },
            notes: Vec::new(),
            diagnostics: Vec::new(),
        })
    }
}

#[test]
fn engine_reinvests_dividend_receivable_in_round_lots() {
    let buy_date = d(2025, 1, 1);
    let ex_date = d(2025, 1, 2);
    let payable_date = d(2025, 1, 3);
    let data = DataSet::from_components_with_actions(
        vec![Instrument {
            symbol: "000001.SZ".to_string(),
            name: "Anchor".to_string(),
            board: "SZ".to_string(),
            round_lot: 100,
            listed_at: Some(d(2020, 1, 1)),
            delisted_at: None,
            status: "active".to_string(),
        }],
        vec![
            DailyMarketSnapshot {
                date: buy_date,
                symbol: "000001.SZ".to_string(),
                timestamp: Some("2025-01-01 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: 10.0,
                ask1: 10.0,
                prev_close: 10.0,
                volume: 100_000,
                tick_volume: 100_000,
                bid1_volume: 100_000,
                ask1_volume: 100_000,
                trading_phase: Some("continuous".to_string()),
                paused: false,
                upper_limit: 11.0,
                lower_limit: 9.0,
                price_tick: 0.01,
            },
            DailyMarketSnapshot {
                date: ex_date,
                symbol: "000001.SZ".to_string(),
                timestamp: Some("2025-01-02 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: 10.0,
                ask1: 10.0,
                prev_close: 10.0,
                volume: 100_000,
                tick_volume: 100_000,
                bid1_volume: 100_000,
                ask1_volume: 100_000,
                trading_phase: Some("continuous".to_string()),
                paused: false,
                upper_limit: 11.0,
                lower_limit: 9.0,
                price_tick: 0.01,
            },
            DailyMarketSnapshot {
                date: payable_date,
                symbol: "000001.SZ".to_string(),
                timestamp: Some("2025-01-03 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: 10.0,
                ask1: 10.0,
                prev_close: 10.0,
                volume: 100_000,
                tick_volume: 100_000,
                bid1_volume: 100_000,
                ask1_volume: 100_000,
                trading_phase: Some("continuous".to_string()),
                paused: false,
                upper_limit: 11.0,
                lower_limit: 9.0,
                price_tick: 0.01,
            },
        ],
        vec![
            DailyFactorSnapshot {
                date: buy_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: ex_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: payable_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(),
            },
        ],
        vec![
            CandidateEligibility {
                date: buy_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: ex_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: payable_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,
            },
        ],
        vec![
            BenchmarkSnapshot {
                date: buy_date,
                benchmark: "000300.SH".to_string(),
                open: 100.0,
                close: 100.0,
                prev_close: 99.0,
                volume: 1_000_000,
            },
            BenchmarkSnapshot {
                date: ex_date,
                benchmark: "000300.SH".to_string(),
                open: 100.0,
                close: 100.0,
                prev_close: 99.0,
                volume: 1_000_000,
            },
            BenchmarkSnapshot {
                date: payable_date,
                benchmark: "000300.SH".to_string(),
                open: 100.0,
                close: 100.0,
                prev_close: 100.0,
                volume: 1_000_000,
            },
        ],
        vec![CorporateAction {
            date: ex_date,
            symbol: "000001.SZ".to_string(),
            payable_date: Some(payable_date),
            share_cash: 1.05,
            share_bonus: 0.0,
            share_gift: 0.0,
            issue_quantity: 0.0,
            issue_price: 0.0,
            reform: false,
            adjust_factor: None,
            successor_symbol: None,
            successor_ratio: None,
            successor_cash: None,
        }],
    )
    .expect("dataset");

    let mut engine = BacktestEngine::new(
        data,
        BuyAndHoldStrategy {
            first_date: buy_date,
        },
        BrokerSimulator::new_with_execution_price(
            ChinaAShareCostModel::default(),
            ChinaEquityRuleHooks::default(),
            PriceField::Open,
        ),
        BacktestConfig {
            initial_cash: 11_005.0,
            benchmark_code: "000300.SH".to_string(),
            start_date: Some(buy_date),
            end_date: Some(payable_date),
            decision_lag_trading_days: 0,
            execution_price_field: PriceField::Open,
        },
    )
    .with_dividend_reinvestment(true);

    let result = engine.run().expect("backtest run");
    let final_holding = result
        .holdings_summary
        .iter()
        .find(|row| row.symbol == "000001.SZ")
        .expect("holding");
    assert_eq!(final_holding.quantity, 1_100);
    assert!((result.equity_curve.last().expect("equity").cash - 1_050.0).abs() < 1e-9);
    let reinvest_fill = result
        .fills
        .iter()
        .find(|fill| fill.reason == "dividend_reinvestment")
        .expect("reinvestment fill");
    assert_eq!(reinvest_fill.quantity, 100);
    assert_eq!(reinvest_fill.commission, 0.0);
    assert_eq!(reinvest_fill.stamp_tax, 0.0);
}
