use std::collections::BTreeMap;

use chrono::NaiveDate;
use fidc_core::cost::CostModel;
use fidc_core::rules::EquityRuleHooks;
use fidc_core::{
    CandidateEligibility, ChinaAShareCostModel, ChinaEquityRuleHooks, DailyMarketSnapshot,
    OrderSide, Position, PriceField,
};

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

fn candidate() -> CandidateEligibility {
    CandidateEligibility {
        date: d(2024, 1, 3),
        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,
    }
}

fn snapshot(open: f64, upper_limit: f64, lower_limit: f64) -> DailyMarketSnapshot {
    DailyMarketSnapshot {
        date: d(2024, 1, 3),
        symbol: "000001.SZ".to_string(),
        timestamp: Some("2024-01-03 10:18:00".to_string()),
        day_open: open,
        open,
        high: open,
        low: open,
        close: open,
        last_price: open,
        bid1: open,
        ask1: open,
        prev_close: 10.0,
        volume: 1_000_000,
        tick_volume: 100_000,
        bid1_volume: 50_000,
        ask1_volume: 50_000,
        trading_phase: Some("continuous".to_string()),
        paused: false,
        upper_limit,
        lower_limit,
        price_tick: 0.01,
    }
}

#[test]
fn china_cost_model_applies_minimum_commission_and_stamp_tax() {
    let model = ChinaAShareCostModel::default();

    let buy = model.calculate(d(2023, 8, 25), OrderSide::Buy, 1_000.0);
    assert!((buy.commission - 5.0).abs() < 1e-9);
    assert_eq!(buy.stamp_tax, 0.0);

    let sell = model.calculate(d(2023, 8, 25), OrderSide::Sell, 100_000.0);
    assert!((sell.commission - 30.0).abs() < 1e-9);
    assert!((sell.stamp_tax - 100.0).abs() < 1e-9);
}

#[test]
fn china_cost_model_switches_stamp_tax_rate_after_2023_08_28() {
    let model = ChinaAShareCostModel::default();

    let before = model.calculate(d(2023, 8, 25), OrderSide::Sell, 100_000.0);
    let after = model.calculate(d(2023, 8, 28), OrderSide::Sell, 100_000.0);

    assert!((before.stamp_tax - 100.0).abs() < 1e-9);
    assert!((after.stamp_tax - 50.0).abs() < 1e-9);
}

#[test]
fn china_cost_model_tracks_minimum_commission_per_order_id() {
    let model = ChinaAShareCostModel::default();
    let mut commission_state = BTreeMap::new();

    let first = model.calculate_with_order_state(
        d(2024, 1, 3),
        OrderSide::Buy,
        1_000.0,
        Some(7),
        &mut commission_state,
    );
    let second = model.calculate_with_order_state(
        d(2024, 1, 3),
        OrderSide::Buy,
        1_000.0,
        Some(7),
        &mut commission_state,
    );
    let third = model.calculate_with_order_state(
        d(2024, 1, 3),
        OrderSide::Buy,
        20_000.0,
        Some(7),
        &mut commission_state,
    );
    let another_order = model.calculate_with_order_state(
        d(2024, 1, 3),
        OrderSide::Buy,
        1_000.0,
        Some(8),
        &mut commission_state,
    );

    assert!((first.commission - 5.0).abs() < 1e-9);
    assert!(second.commission.abs() < 1e-9);
    assert!((third.commission - 1.6).abs() < 1e-9);
    assert!((another_order.commission - 5.0).abs() < 1e-9);
}

#[test]
fn china_rule_hooks_block_same_day_sell_under_t_plus_one() {
    let hooks = ChinaEquityRuleHooks;
    let mut position = Position::new("000001.SZ");
    let trade_date = d(2024, 1, 3);
    position.buy(trade_date, 1_000, 10.0);

    let check = hooks.can_sell(
        trade_date,
        &snapshot(10.1, 11.0, 9.0),
        &candidate(),
        &position,
        PriceField::Open,
    );

    assert!(!check.allowed);
    assert!(check.reason.as_deref().unwrap_or_default().contains("t+1"));
}

#[test]
fn china_rule_hooks_block_buy_at_limit_up_and_sell_at_limit_down() {
    let hooks = ChinaEquityRuleHooks;
    let candidate = candidate();
    let mut position = Position::new("000001.SZ");
    position.buy(d(2024, 1, 2), 1_000, 10.0);

    let buy_check = hooks.can_buy(
        d(2024, 1, 3),
        &snapshot(11.0, 11.0, 9.0),
        &candidate,
        PriceField::Open,
    );
    assert!(!buy_check.allowed);
    assert!(
        buy_check
            .reason
            .as_deref()
            .unwrap_or_default()
            .contains("upper limit")
    );

    let sell_check = hooks.can_sell(
        d(2024, 1, 3),
        &snapshot(9.0, 11.0, 9.0),
        &candidate,
        &position,
        PriceField::Open,
    );
    assert!(!sell_check.allowed);
    assert!(
        sell_check
            .reason
            .as_deref()
            .unwrap_or_default()
            .contains("lower limit")
    );
}

#[test]
fn china_rule_hooks_use_tick_size_tolerance_for_price_limits() {
    let hooks = ChinaEquityRuleHooks;
    let candidate = candidate();

    let near_upper = DailyMarketSnapshot {
        price_tick: 0.001,
        ..snapshot(10.9995, 11.0, 9.0)
    };
    let buy_check = hooks.can_buy(d(2024, 1, 3), &near_upper, &candidate, PriceField::Open);
    assert!(!buy_check.allowed);

    let near_lower = DailyMarketSnapshot {
        price_tick: 0.001,
        ..snapshot(9.0005, 11.0, 9.0)
    };
    let mut position = Position::new("000001.SZ");
    position.buy(d(2024, 1, 2), 1_000, 10.0);
    let sell_check = hooks.can_sell(
        d(2024, 1, 3),
        &near_lower,
        &candidate,
        &position,
        PriceField::Open,
    );
    assert!(!sell_check.allowed);
}

#[test]
fn china_rule_hooks_allow_sell_when_last_price_is_above_lower_limit() {
    let hooks = ChinaEquityRuleHooks;
    let candidate = candidate();
    let mut position = Position::new("000001.SZ");
    position.buy(d(2024, 4, 3), 1_000, 2.89);

    let snapshot = DailyMarketSnapshot {
        date: d(2024, 4, 7),
        symbol: "000001.SZ".to_string(),
        timestamp: Some("2024-04-07 10:18:00".to_string()),
        day_open: 2.53,
        open: 2.53,
        high: 2.53,
        low: 2.52,
        close: 2.53,
        last_price: 2.53,
        bid1: 2.52,
        ask1: 2.53,
        prev_close: 2.80,
        volume: 1_000_000,
        tick_volume: 100_000,
        bid1_volume: 50_000,
        ask1_volume: 50_000,
        trading_phase: Some("continuous".to_string()),
        paused: false,
        upper_limit: 3.08,
        lower_limit: 2.52,
        price_tick: 0.01,
    };

    let sell_check = hooks.can_sell(
        d(2024, 4, 7),
        &snapshot,
        &candidate,
        &position,
        PriceField::Last,
    );
    assert!(
        sell_check.allowed,
        "sell should be allowed when snapshot last is above lower limit"
    );
}
