//! Integration tests for ib-flex parser

use ib_flex::types::TradeType;
use ib_flex::{parse_activity_flex, AssetCategory, BuySell, OpenClose, PutCall};
use rust_decimal::Decimal;
use std::str::FromStr;

#[test]
fn test_parse_minimal_statement() {
    let xml = include_str!("fixtures/activity_minimal.xml");
    let result = parse_activity_flex(xml);

    assert!(result.is_ok(), "Failed to parse XML: {:?}", result.err());

    let statement = result.unwrap();
    assert_eq!(statement.account_id.as_deref(), Some("U1234567"));
    assert_eq!(statement.from_date.unwrap().to_string(), "2025-01-15");
    assert_eq!(statement.to_date.unwrap().to_string(), "2025-01-15");
}

#[test]
fn test_parse_symbol_summary_with_multi_settlement_date() {
    // IBKR can emit self-closing aggregate summary siblings with
    // `settleDateTarget="MULTI"`. This is not Latin-1 corruption or an illegal
    // XML character; it means the summary has no single settlement date because
    // it spans multiple underlying trade/lot rows.
    let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<FlexQueryResponse queryName="Activity Query" type="AF">
    <FlexStatements count="1">
        <FlexStatement accountId="U1234567" fromDate="2025-01-15" toDate="2025-01-15"
                       period="SingleDay" whenGenerated="2025-01-15;235959">
            <Trades>
                <Trade accountId="U1234567" currency="USD" fxRateToBase="1"
                       assetCategory="STK" symbol="AAPL" description="APPLE INC" conid="265598"
                       transactionID="567890123" tradeDate="2025-01-15" dateTime="2025-01-15;093015"
                       settleDateTarget="2025-01-17" buySell="BUY" quantity="100" price="185.50"
                       amount="-18550.00" proceeds="-18550.00" ibCommission="-1.00"
                       ibCommissionCurrency="USD" netCash="-18551.00" multiplier="1"
                       tradeID="123456789" execID="test123" />
                <SymbolSummary accountId="U1234567" currency="USD" fxRateToBase="1"
                       assetCategory="CASH" symbol="USD.TWD" description="USD.TWD" conid="12087792"
                       levelOfDetail="SYMBOL_SUMMARY" tradeDate="20260527"
                       settleDateTarget="MULTI" quantity="0" price="0" proceeds="0"
                       ibCommission="0" netCash="0" />
            </Trades>
        </FlexStatement>
    </FlexStatements>
</FlexQueryResponse>"#;

    let statement = parse_activity_flex(xml).unwrap();
    let trade = &statement.trades.items[0];

    assert_eq!(statement.trades.items.len(), 1);
    assert_eq!(trade.symbol.as_deref(), Some("AAPL"));
}

#[test]
fn test_trades_container_captures_lots_and_summaries() {
    // IB interleaves <Trade>, <Lot>, <Order>, <SymbolSummary> and
    // <AssetSummary> inside the <Trades> container. They are all trade-shaped
    // and should be captured in their own buckets rather than dropped.
    let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<FlexQueryResponse queryName="Activity Query" type="AF">
    <FlexStatements count="1">
        <FlexStatement accountId="U1234567" fromDate="20250115" toDate="20250115"
                       period="SingleDay" whenGenerated="20250115;235959">
            <Trades>
                <Trade accountId="U1234567" currency="USD" assetCategory="STK"
                       symbol="AAPL" conid="265598" tradeDate="20250115"
                       buySell="BUY" quantity="100" tradePrice="185.50"
                       ibCommission="-1.00" netCash="-18551.00"
                       levelOfDetail="EXECUTION" />
                <Lot accountId="U1234567" currency="USD" assetCategory="STK"
                     symbol="AAPL" conid="265598" tradeDate="20250115"
                     buySell="BUY" quantity="100" tradePrice="185.50"
                     openDateTime="20240101;100000" levelOfDetail="LOT"
                     fifoPnlRealized="12.34" />
                <SymbolSummary accountId="U1234567" currency="USD"
                       assetCategory="STK" symbol="AAPL" conid="265598"
                       quantity="100" levelOfDetail="SYMBOL_SUMMARY" />
                <Order accountId="U1234567" currency="USD" assetCategory="STK"
                       symbol="AAPL" conid="265598" buySell="BUY" quantity="100"
                       tradePrice="185.50" levelOfDetail="ORDER" />
            </Trades>
        </FlexStatement>
    </FlexStatements>
</FlexQueryResponse>"#;

    let statement = parse_activity_flex(xml).unwrap();
    let t = &statement.trades;
    assert_eq!(t.items.len(), 1, "one execution-level trade");
    assert_eq!(t.lots.len(), 1, "one lot detail row");
    assert_eq!(t.symbol_summaries.len(), 1, "one symbol summary");
    assert_eq!(t.orders.len(), 1, "one order roll-up");

    // Lot rows carry real trade-shaped data (e.g. realized P&L and open date).
    assert_eq!(
        t.lots[0].fifo_pnl_realized,
        Some(Decimal::from_str("12.34").unwrap())
    );
    assert_eq!(t.lots[0].open_date_time.as_deref(), Some("20240101;100000"));
    assert_eq!(
        t.orders[0].price,
        Some(Decimal::from_str("185.50").unwrap())
    );
}

#[test]
fn test_parse_newly_modeled_sections() {
    // Sections that were previously parsed-and-dropped are now captured.
    let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<FlexQueryResponse queryName="Activity Query" type="AF">
    <FlexStatements count="1">
        <FlexStatement accountId="U1234567" fromDate="20250115" toDate="20250115"
                       period="SingleDay" whenGenerated="20250115;235959">
            <TransactionTaxes>
                <TransactionTax accountId="U1234567" currency="GBP" symbol="VOD"
                                conid="111" date="20250115" taxableDescription="Stamp Duty"
                                taxAmount="-12.50" country="UK" />
            </TransactionTaxes>
            <NetStockPositions>
                <NetStockPosition accountId="U1234567" assetCategory="STK" symbol="AAPL"
                                  conid="265598" netShares="100" />
            </NetStockPositions>
            <CFDCharges>
                <CFDCharge accountId="U1234567" currency="USD" total="-3.21" />
            </CFDCharges>
        </FlexStatement>
    </FlexStatements>
</FlexQueryResponse>"#;

    let statement = parse_activity_flex(xml).unwrap();
    assert_eq!(statement.transaction_taxes.items.len(), 1);
    assert_eq!(
        statement.transaction_taxes.items[0].tax_amount,
        Some(Decimal::from_str("-12.50").unwrap())
    );
    assert_eq!(statement.net_stock_positions.items.len(), 1);
    assert_eq!(statement.cfd_charges.items.len(), 1);
}

#[test]
fn test_parse_trades() {
    let xml = include_str!("fixtures/activity_minimal.xml");
    let statement = parse_activity_flex(xml).unwrap();

    assert_eq!(statement.trades.items.len(), 1);

    let trade = &statement.trades.items[0];
    assert_eq!(trade.symbol.as_deref(), Some("AAPL"));
    assert_eq!(trade.asset_category, Some(AssetCategory::Stock));
    assert_eq!(trade.buy_sell, Some(BuySell::Buy));
    assert_eq!(trade.quantity.unwrap().to_string(), "100");
    assert_eq!(trade.price.unwrap().to_string(), "185.50");
}

#[test]
fn test_empty_sections() {
    let xml = include_str!("fixtures/activity_minimal.xml");
    let statement = parse_activity_flex(xml).unwrap();

    // These sections should be empty in minimal fixture
    assert_eq!(statement.positions.items.len(), 0);
    assert_eq!(statement.cash_transactions.items.len(), 0);
    assert_eq!(statement.corporate_actions.items.len(), 0);
}

#[test]
fn test_securities_info_parsing() {
    let xml = include_str!("fixtures/activity_simple.xml");
    let result = parse_activity_flex(xml);

    // This may fail if empty strings aren't handled, which is expected
    // The test documents current behavior
    match result {
        Ok(statement) => {
            println!(
                "Successfully parsed with {} securities",
                statement.securities_info.items.len()
            );
        }
        Err(e) => {
            println!("Expected error with complex XML: {}", e);
            // This is OK - empty strings in XML attributes cause parse errors
            // and that's documented behavior for now
        }
    }
}

#[test]
fn test_commission_calculation() {
    let xml = include_str!("fixtures/activity_minimal.xml");
    let statement = parse_activity_flex(xml).unwrap();

    let total_commission: rust_decimal::Decimal = statement
        .trades
        .items
        .iter()
        .filter_map(|t| t.commission)
        .sum();

    assert_eq!(total_commission.to_string(), "-1.00");
}

#[test]
fn test_trade_proceeds() {
    let xml = include_str!("fixtures/activity_minimal.xml");
    let statement = parse_activity_flex(xml).unwrap();

    let trade = &statement.trades.items[0];
    assert_eq!(trade.proceeds.unwrap().to_string(), "-18550.00");
    assert_eq!(
        trade.net_cash.map(|d| d.to_string()),
        Some("-18551.00".to_string())
    );
}

#[test]
fn test_account_id_extraction() {
    let xml = include_str!("fixtures/activity_minimal.xml");
    let statement = parse_activity_flex(xml).unwrap();

    assert!(!statement.account_id.as_deref().unwrap_or("").is_empty());
    assert_eq!(statement.account_id.as_deref(), Some("U1234567"));
}

#[test]
fn test_date_range_parsing() {
    let xml = include_str!("fixtures/activity_minimal.xml");
    let statement = parse_activity_flex(xml).unwrap();

    assert!(statement.from_date <= statement.to_date);
}

// ==================== OPTIONS TESTS ====================

#[test]
fn test_parse_options_trades() {
    let xml = include_str!("fixtures/activity_options.xml");
    let statement = parse_activity_flex(xml).unwrap();

    assert_eq!(statement.trades.items.len(), 4);

    // Test long call
    let call_trade = &statement.trades.items[0];
    assert_eq!(call_trade.symbol.as_deref(), Some("AAPL  250221C00180000"));
    assert_eq!(call_trade.asset_category, Some(AssetCategory::Option));
    assert_eq!(call_trade.put_call, Some(PutCall::Call));
    assert_eq!(call_trade.strike.unwrap().to_string(), "180");
    assert_eq!(call_trade.multiplier.unwrap().to_string(), "100");
    assert_eq!(call_trade.underlying_symbol, Some("AAPL".to_string()));
    assert_eq!(call_trade.open_close, Some(OpenClose::Open));
}

#[test]
fn test_parse_options_short_put() {
    let xml = include_str!("fixtures/activity_options.xml");
    let statement = parse_activity_flex(xml).unwrap();

    // Test short put (index 1)
    let put_trade = &statement.trades.items[1];
    assert_eq!(put_trade.symbol.as_deref(), Some("TSLA  250221P00200000"));
    assert_eq!(put_trade.put_call, Some(PutCall::Put));
    assert_eq!(put_trade.strike.unwrap().to_string(), "200");
    assert_eq!(put_trade.quantity.unwrap().to_string(), "-2");
    assert_eq!(put_trade.underlying_symbol, Some("TSLA".to_string()));
}

#[test]
fn test_parse_options_assignment() {
    let xml = include_str!("fixtures/activity_options.xml");
    let statement = parse_activity_flex(xml).unwrap();

    // Test assignment (index 2)
    let assignment = &statement.trades.items[2];
    assert_eq!(assignment.symbol.as_deref(), Some("MSFT  250115C00400000"));
    assert_eq!(assignment.open_close, Some(OpenClose::Close));
    assert!(assignment.fifo_pnl_realized.is_some());
    assert_eq!(assignment.fifo_pnl_realized.unwrap().to_string(), "1050");
}

#[test]
fn test_parse_options_positions() {
    let xml = include_str!("fixtures/activity_options.xml");
    let statement = parse_activity_flex(xml).unwrap();

    assert_eq!(statement.positions.items.len(), 2);

    // Long call position
    let long_pos = &statement.positions.items[0];
    assert_eq!(long_pos.quantity.unwrap().to_string(), "1");
    assert_eq!(long_pos.asset_category, Some(AssetCategory::Option));

    // Short put position
    let short_pos = &statement.positions.items[1];
    assert_eq!(short_pos.quantity.unwrap().to_string(), "-2");
}

// ==================== FUTURES TESTS ====================

#[test]
fn test_parse_futures_trades() {
    let xml = include_str!("fixtures/activity_futures.xml");
    let statement = parse_activity_flex(xml).unwrap();

    assert_eq!(statement.trades.items.len(), 4);

    // Test ES futures
    let es_trade = &statement.trades.items[0];
    assert_eq!(es_trade.symbol.as_deref(), Some("ESH5"));
    assert_eq!(es_trade.asset_category, Some(AssetCategory::Future));
    assert_eq!(es_trade.multiplier.unwrap().to_string(), "50");
    assert_eq!(es_trade.underlying_symbol, Some("ES".to_string()));
}

#[test]
fn test_parse_futures_commodity() {
    let xml = include_str!("fixtures/activity_futures.xml");
    let statement = parse_activity_flex(xml).unwrap();

    // Test gold futures (index 3) - just verify it parses
    let gc_trade = &statement.trades.items[3];
    assert_eq!(gc_trade.symbol.as_deref(), Some("GCG5"));
    // Note: commodity_type, fineness, weight are in XML but not yet in Trade struct
}

#[test]
fn test_parse_futures_positions() {
    let xml = include_str!("fixtures/activity_futures.xml");
    let statement = parse_activity_flex(xml).unwrap();

    assert_eq!(statement.positions.items.len(), 3);

    // Check long ES position
    let es_pos = &statement.positions.items[0];
    assert_eq!(es_pos.quantity.unwrap().to_string(), "2");
    assert_eq!(es_pos.asset_category, Some(AssetCategory::Future));
}

// ==================== FOREX TESTS ====================

#[test]
fn test_parse_forex_trades() {
    let xml = include_str!("fixtures/activity_forex.xml");
    let statement = parse_activity_flex(xml).unwrap();

    assert_eq!(statement.trades.items.len(), 4);

    // Test EUR.USD
    let eur_trade = &statement.trades.items[0];
    assert_eq!(eur_trade.symbol.as_deref(), Some("EUR.USD"));
    assert_eq!(eur_trade.asset_category, Some(AssetCategory::Cash));
    assert_eq!(eur_trade.quantity.unwrap().to_string(), "50000");
}

#[test]
fn test_parse_forex_positions() {
    let xml = include_str!("fixtures/activity_forex.xml");
    let statement = parse_activity_flex(xml).unwrap();

    assert_eq!(statement.positions.items.len(), 3);

    // Test EUR long position
    let eur_pos = &statement.positions.items[0];
    assert_eq!(eur_pos.quantity.unwrap().to_string(), "50000");
    assert_eq!(eur_pos.currency.as_deref(), Some("EUR"));
}

#[test]
fn test_parse_forex_conversion_rates() {
    let xml = include_str!("fixtures/activity_forex.xml");
    let statement = parse_activity_flex(xml).unwrap();

    assert_eq!(statement.conversion_rates.items.len(), 5);

    // Check EUR rate
    let eur_rate = statement
        .conversion_rates
        .items
        .iter()
        .find(|r| r.from_currency.as_deref() == Some("EUR"))
        .unwrap();
    assert_eq!(eur_rate.rate.unwrap().to_string(), "1.0855");
}

// ==================== BONDS TESTS ====================

#[test]
fn test_parse_bonds_trades() {
    let xml = include_str!("fixtures/activity_bonds.xml");
    let statement = parse_activity_flex(xml).unwrap();

    assert_eq!(statement.trades.items.len(), 4);

    // Test US Treasury
    let treasury = &statement.trades.items[0];
    assert_eq!(treasury.symbol.as_deref(), Some("T 4.5 11/15/33"));
    assert_eq!(treasury.asset_category, Some(AssetCategory::Bond));
    // Note: cusip and accrued_interest are in XML but not yet in Trade struct
}

#[test]
fn test_parse_bonds_corporate() {
    let xml = include_str!("fixtures/activity_bonds.xml");
    let statement = parse_activity_flex(xml).unwrap();

    // Test corporate bond (index 1)
    let corp_bond = &statement.trades.items[1];
    assert_eq!(corp_bond.symbol.as_deref(), Some("AAPL 3.85 08/04/46"));
    // Note: issuer is in XML but not yet in Trade struct
}

#[test]
fn test_parse_bonds_positions() {
    let xml = include_str!("fixtures/activity_bonds.xml");
    let statement = parse_activity_flex(xml).unwrap();

    assert_eq!(statement.positions.items.len(), 3);

    // All should be long bond positions
    for pos in &statement.positions.items {
        assert_eq!(pos.asset_category, Some(AssetCategory::Bond));
        assert!(pos.quantity.unwrap() > rust_decimal::Decimal::ZERO);
    }
}

// ==================== CORPORATE ACTIONS TESTS ====================

#[test]
fn test_parse_corporate_actions() {
    let xml = include_str!("fixtures/activity_corporate_actions.xml");
    let statement = parse_activity_flex(xml).unwrap();

    assert_eq!(statement.corporate_actions.items.len(), 8);
}

#[test]
fn test_parse_dividend_action() {
    let xml = include_str!("fixtures/activity_corporate_actions.xml");
    let statement = parse_activity_flex(xml).unwrap();

    // Find dividend (first item)
    let dividend = &statement.corporate_actions.items[0];
    assert_eq!(dividend.symbol.as_deref(), Some("AAPL"));
    assert_eq!(dividend.action_type, Some("DI".to_string()));
    assert_eq!(dividend.quantity.unwrap().to_string(), "400");
    assert_eq!(dividend.amount.unwrap().to_string(), "100.00");
}

#[test]
fn test_parse_stock_split() {
    let xml = include_str!("fixtures/activity_corporate_actions.xml");
    let statement = parse_activity_flex(xml).unwrap();

    // Find stock split (index 1)
    let split = &statement.corporate_actions.items[1];
    assert_eq!(split.symbol.as_deref(), Some("TSLA"));
    assert_eq!(split.action_type, Some("FS".to_string()));
    // Note: principal_adjust_factor field needs to be checked in struct
}

#[test]
fn test_parse_merger() {
    let xml = include_str!("fixtures/activity_corporate_actions.xml");
    let statement = parse_activity_flex(xml).unwrap();

    // Find merger (index 3)
    let merger = &statement.corporate_actions.items[3];
    assert_eq!(merger.symbol.as_deref(), Some("ACQUIRED"));
    assert_eq!(merger.action_type, Some("TC".to_string()));
    assert_eq!(merger.fifo_pnl_realized.unwrap().to_string(), "1500.00");
}

// ==================== CASH TRANSACTIONS TESTS ====================

#[test]
fn test_parse_cash_transactions() {
    let xml = include_str!("fixtures/activity_cash.xml");
    let statement = parse_activity_flex(xml).unwrap();

    assert_eq!(statement.cash_transactions.items.len(), 15);
}

#[test]
fn test_parse_deposit_withdrawal() {
    let xml = include_str!("fixtures/activity_cash.xml");
    let statement = parse_activity_flex(xml).unwrap();

    // Find deposit (first item)
    let deposit = &statement.cash_transactions.items[0];
    assert_eq!(deposit.amount.unwrap().to_string(), "10000.00");
    assert_eq!(
        deposit.transaction_type,
        Some("Deposits & Withdrawals".to_string())
    );

    // Find withdrawal (index 1)
    let withdrawal = &statement.cash_transactions.items[1];
    assert_eq!(withdrawal.amount.unwrap().to_string(), "-5000.00");
}

#[test]
fn test_parse_interest_transactions() {
    let xml = include_str!("fixtures/activity_cash.xml");
    let statement = parse_activity_flex(xml).unwrap();

    // Find credit interest (index 2)
    let credit = &statement.cash_transactions.items[2];
    assert_eq!(
        credit.transaction_type,
        Some("Broker Interest Paid".to_string())
    );
    assert!(credit.amount.unwrap() > rust_decimal::Decimal::ZERO);
}

#[test]
fn test_parse_dividend_cash() {
    let xml = include_str!("fixtures/activity_cash.xml");
    let statement = parse_activity_flex(xml).unwrap();

    // Find dividend (index 4)
    let dividend = &statement.cash_transactions.items[4];
    assert_eq!(dividend.symbol.clone().unwrap(), "AAPL");
    assert_eq!(dividend.transaction_type, Some("Dividends".to_string()));
    assert_eq!(dividend.amount.unwrap().to_string(), "150.00");
}

// ==================== WARRANT TESTS ====================

#[test]
fn test_parse_warrants() {
    let xml = include_str!("fixtures/activity_warrants.xml");
    let statement = parse_activity_flex(xml).unwrap();

    assert_eq!(statement.trades.items.len(), 2);

    // Buy warrant
    let buy = &statement.trades.items[0];
    assert_eq!(buy.asset_category, Some(AssetCategory::Warrant));
    assert_eq!(buy.symbol.as_deref(), Some("ABC WS"));
    assert_eq!(buy.buy_sell, Some(BuySell::Buy));
    assert_eq!(buy.quantity.unwrap().to_string(), "100");

    // Sell warrant
    let sell = &statement.trades.items[1];
    assert_eq!(sell.asset_category, Some(AssetCategory::Warrant));
    assert_eq!(sell.buy_sell, Some(BuySell::Sell));
    assert!(sell.fifo_pnl_realized.is_some());
}

#[test]
fn test_parse_warrant_position() {
    let xml = include_str!("fixtures/activity_warrants.xml");
    let statement = parse_activity_flex(xml).unwrap();

    assert_eq!(statement.positions.items.len(), 1);

    let position = &statement.positions.items[0];
    assert_eq!(position.asset_category, Some(AssetCategory::Warrant));
    assert_eq!(position.symbol.as_deref(), Some("ABC WS"));
    assert_eq!(position.quantity.unwrap().to_string(), "50");
}

// ==================== TREASURY BILL TESTS ====================

#[test]
fn test_parse_tbills() {
    let xml = include_str!("fixtures/activity_tbills.xml");
    let statement = parse_activity_flex(xml).unwrap();

    assert_eq!(statement.trades.items.len(), 1);

    let trade = &statement.trades.items[0];
    assert_eq!(trade.asset_category, Some(AssetCategory::Bill));
    assert_eq!(trade.symbol.as_deref(), Some("912796ZX9"));
    assert_eq!(trade.buy_sell, Some(BuySell::Buy));
    assert_eq!(trade.quantity.unwrap().to_string(), "10000");
}

#[test]
fn test_parse_tbill_position() {
    let xml = include_str!("fixtures/activity_tbills.xml");
    let statement = parse_activity_flex(xml).unwrap();

    assert_eq!(statement.positions.items.len(), 1);

    let position = &statement.positions.items[0];
    assert_eq!(position.asset_category, Some(AssetCategory::Bill));
    assert_eq!(position.quantity.unwrap().to_string(), "10000");
}

#[test]
fn test_parse_tbill_maturity() {
    let xml = include_str!("fixtures/activity_tbills.xml");
    let statement = parse_activity_flex(xml).unwrap();

    assert_eq!(statement.corporate_actions.items.len(), 1);

    let action = &statement.corporate_actions.items[0];
    assert_eq!(action.asset_category, Some(AssetCategory::Bill));
    assert_eq!(action.action_type, Some("TC".to_string()));
}

// ==================== CFD TESTS ====================

#[test]
fn test_parse_cfds() {
    let xml = include_str!("fixtures/activity_cfds.xml");
    let statement = parse_activity_flex(xml).unwrap();

    assert_eq!(statement.trades.items.len(), 2);

    // Buy CFD
    let buy = &statement.trades.items[0];
    assert_eq!(buy.asset_category, Some(AssetCategory::Cfd));
    assert_eq!(buy.symbol.as_deref(), Some("AAPL"));
    assert_eq!(buy.buy_sell, Some(BuySell::Buy));

    // Sell CFD (close)
    let sell = &statement.trades.items[1];
    assert_eq!(sell.asset_category, Some(AssetCategory::Cfd));
    assert_eq!(sell.buy_sell, Some(BuySell::Sell));
    assert!(sell.fifo_pnl_realized.is_some());
}

#[test]
fn test_parse_cfd_financing() {
    let xml = include_str!("fixtures/activity_cfds.xml");
    let statement = parse_activity_flex(xml).unwrap();

    assert_eq!(statement.cash_transactions.items.len(), 1);

    let financing = &statement.cash_transactions.items[0];
    assert_eq!(financing.asset_category, Some(AssetCategory::Cfd));
    assert_eq!(financing.transaction_type, Some("Other Fees".to_string()));
}

// ==================== CANCELLED TRADES TESTS ====================

#[test]
fn test_parse_cancelled_trades() {
    let xml = include_str!("fixtures/activity_cancelled_trades.xml");
    let statement = parse_activity_flex(xml).unwrap();

    assert_eq!(statement.trades.items.len(), 4);

    // Normal buy
    let buy = &statement.trades.items[0];
    assert_eq!(buy.buy_sell, Some(BuySell::Buy));
    assert_eq!(buy.symbol.as_deref(), Some("MSFT"));

    // Cancelled buy
    let cancel_buy = &statement.trades.items[1];
    assert_eq!(cancel_buy.buy_sell, Some(BuySell::CancelBuy));
    assert_eq!(cancel_buy.symbol.as_deref(), Some("MSFT"));

    // Normal sell
    let sell = &statement.trades.items[2];
    assert_eq!(sell.buy_sell, Some(BuySell::Sell));
    assert_eq!(sell.symbol.as_deref(), Some("GOOGL"));

    // Cancelled sell
    let cancel_sell = &statement.trades.items[3];
    assert_eq!(cancel_sell.buy_sell, Some(BuySell::CancelSell));
    assert_eq!(cancel_sell.symbol.as_deref(), Some("GOOGL"));
}

#[test]
fn test_parse_friday_fx_trade_cancel_with_trade_price() {
    let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<FlexQueryResponse queryName="Activity Query" type="AF">
    <FlexStatements count="1">
        <FlexStatement accountId="U1234567" fromDate="20260529" toDate="20260529"
                       period="SingleDay" whenGenerated="20260601;000000">
            <Trades>
                <Trade accountId="U1234567" acctAlias="Fund" model="" currency="TWD"
                       fxRateToBase="0.031812" assetCategory="CASH" symbol="USD.TWD"
                       description="USD.TWD" conid="37928794" tradeID="" multiplier="1"
                       reportDate="20260529" dateTime="20260528;170000"
                       tradeDate="20260528" settleDateTarget="20260528"
                       transactionType="TradeCancel" exchange="--" quantity="0"
                       tradePrice="31.41202078" tradeMoney="-0.000000063"
                       proceeds="0.000000063" taxes="0" ibCommission="0"
                       ibCommissionCurrency="USD" netCash="0" closePrice="0"
                       openCloseIndicator="" notes="Ca" cost="0" fifoPnlRealized="0"
                       mtmPnl="0" origTradePrice="31.41202078" origTradeDate="20260528"
                       origTradeID="9590116305" origOrderID="5240898668"
                       origTransactionID="40261375730" buySell="SELL (Ca.)"
                       ibOrderID="5240898668" transactionID="40271461617"
                       ibExecID="" exchOrderId="N/A" extExecID="N/A"
                       levelOfDetail="EXECUTION" changeInPrice="0" changeInQuantity="0"
                       isAPIOrder="N" accruedInt="0" fineness="0.0" weight="0.0" />
                <Trade accountId="U1234567" acctAlias="Fund" model="" currency="TWD"
                       fxRateToBase="0.031812" assetCategory="CASH" symbol="USD.TWD"
                       description="USD.TWD" conid="37928794" tradeID="9590116305"
                       multiplier="1" reportDate="20260529" dateTime="20260528;170000"
                       tradeDate="20260528" settleDateTarget="20260528"
                       transactionType="ExchTrade" exchange="IDEALFX" quantity="0"
                       tradePrice="31.3774104" tradeMoney="0.000000063"
                       proceeds="-0.000000063" taxes="0" ibCommission="0"
                       ibCommissionCurrency="USD" netCash="0" closePrice="0"
                       openCloseIndicator="" notes="" cost="0" fifoPnlRealized="0"
                       mtmPnl="0" origTradePrice="0" origOrderID="0"
                       origTransactionID="0" buySell="BUY" ibOrderID="5240898668"
                       transactionID="40271461619" ibExecID="CUSTHSFX.021206da.01.02"
                       exchOrderId="N/A" extExecID="N/A" orderTime="20260528;170000"
                       levelOfDetail="EXECUTION" changeInPrice="0" changeInQuantity="0"
                       isAPIOrder="N" accruedInt="0" fineness="0.0" weight="0.0" />
            </Trades>
        </FlexStatement>
    </FlexStatements>
</FlexQueryResponse>"#;

    let statement = parse_activity_flex(xml).unwrap();
    assert_eq!(statement.trades.items.len(), 2);

    let cancel = &statement.trades.items[0];
    assert_eq!(cancel.transaction_type, Some(TradeType::TradeCancel));
    assert_eq!(cancel.buy_sell, Some(BuySell::CancelSell));
    assert_eq!(cancel.quantity, Some(Decimal::ZERO));
    assert_eq!(
        cancel.price,
        Some(Decimal::from_str("31.41202078").unwrap())
    );
    assert_eq!(
        cancel.trade_money,
        Some(Decimal::from_str("-0.000000063").unwrap())
    );
    assert_eq!(cancel.orig_transaction_id.as_deref(), Some("40261375730"));

    let correction = &statement.trades.items[1];
    assert_eq!(correction.transaction_type, Some(TradeType::ExchTrade));
    assert_eq!(correction.buy_sell, Some(BuySell::Buy));
    assert_eq!(correction.quantity, Some(Decimal::ZERO));
    assert_eq!(
        correction.price,
        Some(Decimal::from_str("31.3774104").unwrap())
    );
    assert_eq!(
        correction.trade_money,
        Some(Decimal::from_str("0.000000063").unwrap())
    );
}

// ==================== FRACTIONAL SHARES TESTS ====================

#[test]
fn test_parse_fractional_shares() {
    let xml = include_str!("fixtures/activity_fractional_shares.xml");
    let statement = parse_activity_flex(xml).unwrap();

    assert_eq!(statement.trades.items.len(), 4);

    // Buy fractional shares
    let buy = &statement.trades.items[0];
    assert_eq!(buy.symbol.as_deref(), Some("AMZN"));
    assert_eq!(buy.quantity.unwrap().to_string(), "2.5");

    // Sell fractional shares
    let sell = &statement.trades.items[1];
    assert_eq!(sell.quantity.unwrap().to_string(), "-1.25");
}

#[test]
fn test_parse_fractional_cancellation() {
    let xml = include_str!("fixtures/activity_fractional_shares.xml");
    let statement = parse_activity_flex(xml).unwrap();

    // Find the cancellation
    let cancel = &statement.trades.items[3];
    assert_eq!(cancel.buy_sell, Some(BuySell::CancelBuy));
}

#[test]
fn test_parse_fractional_position() {
    let xml = include_str!("fixtures/activity_fractional_shares.xml");
    let statement = parse_activity_flex(xml).unwrap();

    assert_eq!(statement.positions.items.len(), 1);

    let position = &statement.positions.items[0];
    assert_eq!(position.symbol.as_deref(), Some("AMZN"));
    assert_eq!(position.quantity.unwrap().to_string(), "1.25");
}

// ==================== COMPLEX CORPORATE ACTIONS TESTS ====================

#[test]
fn test_parse_complex_corporate_actions() {
    let xml = include_str!("fixtures/activity_complex_corporate_actions.xml");
    let statement = parse_activity_flex(xml).unwrap();

    assert_eq!(statement.corporate_actions.items.len(), 10);
}

#[test]
fn test_parse_choice_dividend() {
    let xml = include_str!("fixtures/activity_complex_corporate_actions.xml");
    let statement = parse_activity_flex(xml).unwrap();

    // Choice dividend announcement
    let choice = &statement.corporate_actions.items[0];
    assert_eq!(choice.action_type, Some("CD".to_string()));

    // Choice dividend delivery
    let _delivery = &statement.corporate_actions.items[1];
}

#[test]
fn test_parse_tender_offer() {
    let xml = include_str!("fixtures/activity_complex_corporate_actions.xml");
    let statement = parse_activity_flex(xml).unwrap();

    // Tender announcement
    let tender = &statement.corporate_actions.items[2];
    assert_eq!(tender.action_type, Some("TO".to_string()));
    assert_eq!(tender.quantity.unwrap().to_string(), "-50");

    // Tender issue (proceeds)
    let issue = &statement.corporate_actions.items[3];
    assert_eq!(issue.proceeds.unwrap().to_string(), "3500.00");
}

#[test]
fn test_parse_bond_conversion() {
    let xml = include_str!("fixtures/activity_complex_corporate_actions.xml");
    let statement = parse_activity_flex(xml).unwrap();

    // Bond conversion (surrender bonds)
    let conversion = &statement.corporate_actions.items[4];
    assert_eq!(conversion.action_type, Some("BC".to_string()));
    assert_eq!(conversion.asset_category, Some(AssetCategory::Bond));

    // Convertible issue (receive stock)
    let issue = &statement.corporate_actions.items[5];
    assert_eq!(issue.asset_category, Some(AssetCategory::Stock));
    assert_eq!(issue.quantity.unwrap().to_string(), "250");
}

#[test]
fn test_parse_bond_maturity() {
    let xml = include_str!("fixtures/activity_complex_corporate_actions.xml");
    let statement = parse_activity_flex(xml).unwrap();

    let maturity = &statement.corporate_actions.items[6];
    assert_eq!(maturity.action_type, Some("BM".to_string()));
    assert_eq!(maturity.asset_category, Some(AssetCategory::Bond));
    assert_eq!(maturity.proceeds.unwrap().to_string(), "5000.00");
}

#[test]
fn test_parse_coupon_payment() {
    let xml = include_str!("fixtures/activity_complex_corporate_actions.xml");
    let statement = parse_activity_flex(xml).unwrap();

    let coupon = &statement.corporate_actions.items[7];
    assert_eq!(coupon.action_type, Some("CP".to_string()));
    assert_eq!(coupon.proceeds.unwrap().to_string(), "200.00");
}

#[test]
fn test_parse_rights_issue() {
    let xml = include_str!("fixtures/activity_complex_corporate_actions.xml");
    let statement = parse_activity_flex(xml).unwrap();

    // Rights issue
    let rights = &statement.corporate_actions.items[8];
    assert_eq!(rights.action_type, Some("RI".to_string()));

    // Subscribe rights
    let subscribe = &statement.corporate_actions.items[9];
    assert_eq!(subscribe.action_type, Some("SR".to_string()));
}
