//! Tests for Self-Trade Prevention (STP) feature.

#[cfg(test)]
mod tests {
    use crate::orderbook::book::OrderBook;
    use crate::orderbook::error::OrderBookError;
    use crate::orderbook::stp::STPMode;
    use pricelevel::{
        Hash32, Id, OrderType, OrderUpdate, Price, Quantity, Side, TimeInForce, TimestampMs,
    };

    /// Helper: create a non-zero user hash from a single byte value.
    fn user(byte: u8) -> Hash32 {
        Hash32::new([byte; 32])
    }

    /// Helper: add a resting sell order with a specific user_id.
    fn add_sell_order_with_user(
        book: &OrderBook<()>,
        price: u128,
        quantity: u64,
        user_id: Hash32,
    ) -> Id {
        let order = OrderType::Standard {
            id: Id::new(),
            price: Price::new(price),
            quantity: Quantity::new(quantity),
            side: Side::Sell,
            user_id,
            timestamp: TimestampMs::new(crate::utils::current_time_millis()),
            time_in_force: TimeInForce::Gtc,
            extra_fields: (),
        };
        let id = order.id();
        let result = book.add_order(order);
        assert!(result.is_ok(), "failed to add sell order: {result:?}");
        id
    }

    /// Helper: add a resting buy order with a specific user_id.
    fn add_buy_order_with_user(
        book: &OrderBook<()>,
        price: u128,
        quantity: u64,
        user_id: Hash32,
    ) -> Id {
        let order = OrderType::Standard {
            id: Id::new(),
            price: Price::new(price),
            quantity: Quantity::new(quantity),
            side: Side::Buy,
            user_id,
            timestamp: TimestampMs::new(crate::utils::current_time_millis()),
            time_in_force: TimeInForce::Gtc,
            extra_fields: (),
        };
        let id = order.id();
        let result = book.add_order(order);
        assert!(result.is_ok(), "failed to add buy order: {result:?}");
        id
    }

    /// Helper: add a resting sell order with an explicit user_id and timestamp,
    /// used to pin price-time priority in the determinism tests below.
    fn add_sell_order_with_ts(
        book: &OrderBook<()>,
        price: u128,
        quantity: u64,
        user_id: Hash32,
        timestamp: u64,
    ) -> Id {
        let order = OrderType::Standard {
            id: Id::new(),
            price: Price::new(price),
            quantity: Quantity::new(quantity),
            side: Side::Sell,
            user_id,
            timestamp: TimestampMs::new(timestamp),
            time_in_force: TimeInForce::Gtc,
            extra_fields: (),
        };
        let id = order.id();
        let result = book.add_order(order);
        assert!(result.is_ok(), "failed to add sell order: {result:?}");
        id
    }

    // -----------------------------------------------------------------------
    // STP determinism (#94) + consumption fidelity (#132) — the per-level scan
    // must follow the sweep's consumption order (insertion sequence), not the
    // DashMap iteration order. The pre-scan reads `snapshot_by_insertion_seq()`
    // (pricelevel 0.8.1), so `safe_quantity` and the CancelBoth maker selection
    // are deterministic AND match what `match_order` actually consumes — even
    // under non-monotonic timestamps. (#94 first made it deterministic via the
    // timestamp-sorted `snapshot_orders()`; #132 made it sweep-faithful.)
    // -----------------------------------------------------------------------

    /// CancelTaker accumulates `safe_quantity` over the orders that precede the
    /// first same-user maker *in insertion (sweep) order* — here the orders are
    /// added in timestamp order so the two coincide — so the taker fills exactly
    /// the older non-self liquidity before the self-trade is prevented.
    #[test]
    fn test_cancel_taker_scan_follows_time_priority_at_one_level() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelTaker);

        let taker_user = user(9);
        let other = user(1);

        // All at price 100, added in timestamp order. The taker's own resting
        // order sits third by time, so only m1 + m2 are safe to fill.
        let m1 = add_sell_order_with_ts(&book, 100, 3, other, 10);
        let m2 = add_sell_order_with_ts(&book, 100, 4, other, 20);
        let m_self = add_sell_order_with_ts(&book, 100, 5, taker_user, 30);
        let m3 = add_sell_order_with_ts(&book, 100, 6, other, 40);

        let taker_id = Id::new();
        let result = book.match_market_order_with_user(taker_id, 50, Side::Buy, taker_user);

        assert!(result.is_ok(), "expected partial fill, got {result:?}");
        let mr = result.unwrap();
        assert_eq!(mr.executed_quantity().unwrap(), Quantity::new(7));

        let trades = mr.trades().as_vec();
        assert_eq!(
            trades.len(),
            2,
            "should fill exactly the two older non-self orders"
        );
        assert_eq!(trades[0].maker_order_id(), m1);
        assert_eq!(trades[0].quantity(), Quantity::new(3));
        assert_eq!(trades[1].maker_order_id(), m2);
        assert_eq!(trades[1].quantity(), Quantity::new(4));

        // The self order (and the later other-user order) are untouched.
        assert!(
            book.get_order(m_self).is_some(),
            "self maker must survive STP"
        );
        assert!(
            book.get_order(m3).is_some(),
            "later order must not be reached"
        );
        assert!(book.get_order(m1).is_none());
        assert!(book.get_order(m2).is_none());
    }

    /// CancelBoth cancels the *earliest* same-user maker in insertion (sweep) order
    /// — here equal to timestamp order — not an arbitrary one chosen by DashMap
    /// order, and fills the older non-self liquidity.
    #[test]
    fn test_cancel_both_cancels_earliest_self_maker_by_time() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelBoth);

        let taker_user = user(9);
        let other = user(1);

        // Price 100, timestamp order: other -> self1 -> self2.
        let o1 = add_sell_order_with_ts(&book, 100, 8, other, 10);
        let self1 = add_sell_order_with_ts(&book, 100, 5, taker_user, 20);
        let self2 = add_sell_order_with_ts(&book, 100, 6, taker_user, 30);

        let taker_id = Id::new();
        let result = book.match_market_order_with_user(taker_id, 50, Side::Buy, taker_user);

        assert!(result.is_ok(), "expected partial fill, got {result:?}");
        let mr = result.unwrap();

        let trades = mr.trades().as_vec();
        assert_eq!(trades.len(), 1);
        assert_eq!(trades[0].maker_order_id(), o1);
        assert_eq!(trades[0].quantity(), Quantity::new(8));

        // The earliest same-user maker is cancelled; the later one survives.
        assert!(
            book.get_order(self1).is_none(),
            "earliest self maker should be cancelled"
        );
        assert!(
            book.get_order(self2).is_some(),
            "later self maker should remain"
        );
        assert!(
            book.get_order(o1).is_none(),
            "older non-self order fully consumed"
        );
    }

    /// #132: the STP scan follows the sweep's INSERTION-sequence consumption order
    /// (via `snapshot_by_insertion_seq`, pricelevel 0.8.1), not timestamp order, so
    /// it stays correct even when timestamps are non-monotonic with insertion
    /// (client-supplied / modify-restamped). Here the same-user order is inserted
    /// SECOND but carries an EARLIER timestamp; the sweep consumes the other-user
    /// order first, so CancelTaker must fill it before the self-cross — which only
    /// insertion-order scanning gets right (a timestamp scan would cancel with 0 fills).
    #[test]
    fn test_cancel_taker_scan_follows_insertion_not_timestamp_order() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelTaker);

        let taker_user = user(9);
        let other = user(1);

        // Same price 100. Insertion order (what the sweep consumes): other, then
        // self — but timestamps are non-monotonic with insertion (self is older).
        let m_other = add_sell_order_with_ts(&book, 100, 4, other, 30);
        let m_self = add_sell_order_with_ts(&book, 100, 5, taker_user, 10);

        let result = book.match_market_order_with_user(Id::new(), 20, Side::Buy, taker_user);

        assert!(
            result.is_ok(),
            "expected a 4-unit partial fill, got {result:?}"
        );
        let mr = result.unwrap();
        let trades = mr.trades().as_vec();
        assert_eq!(trades.len(), 1);
        assert_eq!(trades[0].maker_order_id(), m_other);
        assert_eq!(trades[0].quantity(), Quantity::new(4));

        assert!(
            book.get_order(m_self).is_some(),
            "self order survives CancelTaker"
        );
        assert!(
            book.get_order(m_other).is_none(),
            "other-user order is consumed first by insertion order"
        );
    }

    // -----------------------------------------------------------------------
    // STP maker-cancel side effects (#95) — a CancelMaker / CancelBoth cancel
    // must emit a level-change event, record OrderStatus::Cancelled
    // { SelfTradePrevention }, and release the per-account risk slot, in
    // lockstep with cancel_order_with_reason.
    // -----------------------------------------------------------------------

    /// CancelMaker: the cancelled maker reaches `Cancelled { SelfTradePrevention }`
    /// and a `PriceLevelChangedEvent` is emitted for its level.
    #[test]
    fn test_stp_cancel_maker_emits_event_and_records_state() {
        use crate::orderbook::book_change_event::PriceLevelChangedEvent;
        use crate::orderbook::order_state::{CancelReason, OrderStateTracker, OrderStatus};
        use std::sync::{Arc, Mutex};

        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelMaker);
        book.set_order_state_tracker(OrderStateTracker::new());

        let u = user(7);
        let maker = add_sell_order_with_user(&book, 100, 10, u);

        // Install the listener only after the maker rests, so the captured events
        // come from the STP cancel path, not the admission.
        let events: Arc<Mutex<Vec<PriceLevelChangedEvent>>> = Arc::new(Mutex::new(Vec::new()));
        let sink = Arc::clone(&events);
        book.set_price_level_listener(Arc::new(move |e: PriceLevelChangedEvent| {
            sink.lock().expect("event sink").push(e);
        }));

        // Same user crosses -> CancelMaker cancels the resting maker.
        let taker = Id::new();
        let _ = book.match_market_order_with_user(taker, 10, Side::Buy, u);

        match book.order_status(maker) {
            Some(OrderStatus::Cancelled { reason, .. }) => {
                assert_eq!(reason, CancelReason::SelfTradePrevention);
            }
            other => panic!("expected Cancelled {{ SelfTradePrevention }}, got {other:?}"),
        }

        let captured = events.lock().expect("event sink");
        assert!(
            captured
                .iter()
                .any(|e| e.price == 100 && e.side == Side::Sell),
            "expected a Sell-side PriceLevelChangedEvent at price 100 for the cancelled maker's level"
        );
    }

    /// #107: with same-user makers on BOTH sides of an interleaved other-user
    /// maker (`same, other, same` in insertion/sweep order), CancelMaker cancels
    /// EVERY same-user maker regardless of queue position and the taker fills
    /// only the interleaved non-self maker. Pins the inline snapshot-buffer
    /// cancel (which replaced the per-level `maker_order_ids` Vec) to the exact
    /// CancelMaker set, position-independent, from the OrderBook side.
    #[test]
    fn test_cancel_maker_same_other_same_cancels_both_self_fills_other_issue_107() {
        use crate::orderbook::order_state::{CancelReason, OrderStateTracker, OrderStatus};

        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelMaker);
        book.set_order_state_tracker(OrderStateTracker::new());

        let taker_user = user(9);
        let other = user(1);

        // Insertion (sweep) order at price 100: same -> other -> same.
        let self_a = add_sell_order_with_user(&book, 100, 10, taker_user);
        let other_b = add_sell_order_with_user(&book, 100, 7, other);
        let self_c = add_sell_order_with_user(&book, 100, 20, taker_user);

        // Same-user taker crosses with ample size.
        let taker = Id::new();
        let result = book.match_market_order_with_user(taker, 100, Side::Buy, taker_user);
        assert!(
            result.is_ok(),
            "expected a fill against the non-self maker, got {result:?}"
        );
        let mr = result.unwrap();

        // Only the interleaved other-user maker fills — exactly its 7 units, in
        // one trade. The same-user makers never self-trade.
        let trades = mr.trades().as_vec();
        assert_eq!(trades.len(), 1, "only the non-self maker fills");
        assert_eq!(trades[0].maker_order_id(), other_b);
        assert_eq!(trades[0].quantity(), Quantity::new(7));
        assert_eq!(mr.executed_quantity().unwrap(), Quantity::new(7));

        // Both same-user makers are cancelled (SelfTradePrevention), regardless
        // of their position around the non-self maker.
        for self_id in [self_a, self_c] {
            assert!(
                book.get_order(self_id).is_none(),
                "same-user maker must be STP-cancelled"
            );
            match book.order_status(self_id) {
                Some(OrderStatus::Cancelled { reason, .. }) => {
                    assert_eq!(reason, CancelReason::SelfTradePrevention);
                }
                other => panic!("expected Cancelled {{ SelfTradePrevention }}, got {other:?}"),
            }
        }
        // The non-self maker was fully consumed.
        assert!(
            book.get_order(other_b).is_none(),
            "non-self maker fully filled"
        );
    }

    /// CancelBoth: cancelling the maker releases its per-account risk slot (no
    /// counter leak) and records `Cancelled { SelfTradePrevention }`.
    #[test]
    fn test_stp_cancel_both_frees_risk_slot_and_records_state() {
        use crate::orderbook::order_state::{CancelReason, OrderStateTracker, OrderStatus};
        use crate::orderbook::risk::RiskConfig;

        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelBoth);
        book.set_risk_config(RiskConfig::new().with_max_open_orders_per_account(1));
        book.set_order_state_tracker(OrderStateTracker::new());

        let u = user(7);
        // Consumes the single per-account open-order slot.
        let maker = Id::new();
        book.add_limit_order_with_user(maker, 100, 10, Side::Sell, TimeInForce::Gtc, u, None)
            .expect("maker admitted (1/1)");

        // Same user crosses -> CancelBoth cancels the maker (and the taker).
        let taker = Id::new();
        let _ = book.match_market_order_with_user(taker, 5, Side::Buy, u);

        match book.order_status(maker) {
            Some(OrderStatus::Cancelled { reason, .. }) => {
                assert_eq!(reason, CancelReason::SelfTradePrevention);
            }
            other => panic!("expected Cancelled {{ SelfTradePrevention }}, got {other:?}"),
        }

        // The risk slot must have been released: a new order from the same account
        // is admitted. If on_cancel were skipped, the counter would still read 1/1
        // and this would fail with RiskMaxOpenOrders.
        book.add_limit_order_with_user(Id::new(), 101, 1, Side::Sell, TimeInForce::Gtc, u, None)
            .expect("STP cancel must free the per-account risk slot");
    }

    /// FOK feasibility must be STP-aware (#96): under CancelMaker, a same-user
    /// resting order is NOT fillable (it gets cancelled), so a FOK that could only
    /// be "filled" by crossing its own order must be killed up front — before any
    /// maker is cancelled or any trade is emitted. The raw `peek_match` counted the
    /// self quantity and let the FOK proceed, cancelling the maker and filling 0.
    #[test]
    fn test_fok_under_stp_cancel_maker_kills_without_touching_makers() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelMaker);

        let u = user(7);
        // The only resting liquidity at price 100 belongs to the taker's own user.
        let maker = add_sell_order_with_user(&book, 100, 10, u);

        let fok = OrderType::Standard {
            id: Id::new(),
            price: Price::new(100),
            quantity: Quantity::new(10),
            side: Side::Buy,
            user_id: u,
            time_in_force: TimeInForce::Fok,
            timestamp: TimestampMs::new(crate::utils::current_time_millis()),
            extra_fields: (),
        };
        let result = book.add_order(fok);

        assert!(
            matches!(result, Err(OrderBookError::InsufficientLiquidity { .. })),
            "FOK must be killed when only self-liquidity exists, got {result:?}"
        );
        // Killed before matching: the maker is untouched and nothing traded.
        assert!(
            book.get_order(maker).is_some(),
            "FOK kill must not cancel the resting maker"
        );
        assert!(
            !book.has_traded.load(std::sync::atomic::Ordering::SeqCst),
            "FOK kill must emit no trades"
        );
    }

    /// #97: a taker that partially fills against another user and then would
    /// self-cross under CancelTaker must have its residual CANCELLED, not rested.
    /// Previously the partial-fill path returned Ok with a resting remainder,
    /// defeating STP and never recording the SelfTradePrevention terminal state.
    #[test]
    fn test_cancel_taker_partial_self_cross_does_not_rest_residual() {
        use crate::orderbook::order_state::{CancelReason, OrderStateTracker, OrderStatus};

        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelTaker);
        book.set_order_state_tracker(OrderStateTracker::new());

        let taker_user = user(7);
        let other = user(1);

        // Non-self liquidity at the better price; the taker's own order behind it.
        let other_maker = add_sell_order_with_user(&book, 100, 5, other);
        let self_maker = add_sell_order_with_user(&book, 200, 10, taker_user);

        // GTC buy 20 at limit 200: fills 5 vs `other` at 100, then hits its own
        // order at 200 -> CancelTaker cancels the taker. The 15-unit residual must
        // NOT rest, and the taker records Cancelled { SelfTradePrevention }.
        let taker_id = Id::new();
        let taker = OrderType::Standard {
            id: taker_id,
            price: Price::new(200),
            quantity: Quantity::new(20),
            side: Side::Buy,
            user_id: taker_user,
            time_in_force: TimeInForce::Gtc,
            timestamp: TimestampMs::new(crate::utils::current_time_millis()),
            extra_fields: (),
        };
        let result = book.add_order(taker);

        assert!(
            matches!(result, Err(OrderBookError::SelfTradePrevented { .. })),
            "partial self-cross under CancelTaker must surface the STP error, got {result:?}"
        );
        assert!(
            book.get_order(taker_id).is_none(),
            "STP-cancelled taker must not rest its residual"
        );
        assert!(book.bids.is_empty(), "no residual bid level should exist");

        match book.order_status(taker_id) {
            Some(OrderStatus::Cancelled {
                filled_quantity,
                reason,
            }) => {
                assert_eq!(reason, CancelReason::SelfTradePrevention);
                assert_eq!(
                    filled_quantity, 5,
                    "filled quantity must reflect the 5-unit non-self fill"
                );
            }
            other => panic!("expected Cancelled {{ SelfTradePrevention, 5 }}, got {other:?}"),
        }

        // The non-self maker was consumed; the self maker survives (CancelTaker).
        assert!(book.get_order(other_maker).is_none());
        assert!(book.get_order(self_maker).is_some());
    }

    /// #97 (notional path contract): `match_market_order_by_amount_with_user` is a
    /// MATCH-ONLY API — a notional taker that partially fills then self-crosses
    /// under CancelTaker returns `Ok(partial)` (the unspent notional is simply not
    /// consumed), mirroring the base-qty market-order contract. It never rests a
    /// residual and never emits a self-trade; the residual-cancel of #97 only
    /// applies to the lifecycle-managing `add_order` path.
    #[test]
    fn test_notional_market_partial_self_cross_returns_partial() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelTaker);

        let taker_user = user(7);
        let other = user(1);

        let other_maker = add_sell_order_with_user(&book, 100, 5, other);
        let self_maker = add_sell_order_with_user(&book, 200, 10, taker_user);

        // Notional buy of 1000 quote: 5 @ 100 (500 spent) vs `other`, then the
        // self-cross at 200 cancels the taker. Match-only -> Ok with the 5-unit fill.
        let taker_id = Id::new();
        let result =
            book.match_market_order_by_amount_with_user(taker_id, 1000, Side::Buy, taker_user);

        let mr = result.expect("notional partial self-cross returns Ok(partial)");
        assert_eq!(mr.executed_quantity().unwrap(), Quantity::new(5));
        assert!(book.get_order(taker_id).is_none(), "no residual rests");
        assert!(book.bids.is_empty());
        assert!(book.get_order(other_maker).is_none());
        assert!(book.get_order(self_maker).is_some());
    }

    // -----------------------------------------------------------------------
    // STPMode::None — backward compatibility
    // -----------------------------------------------------------------------

    #[test]
    fn test_stp_none_orders_match_normally() {
        let book: OrderBook<()> = OrderBook::new("TEST");
        // default is STPMode::None
        assert_eq!(book.stp_mode(), STPMode::None);

        let same_user = user(1);
        add_sell_order_with_user(&book, 100, 10, same_user);

        // Same user submits a buy market order — should match (no STP)
        let taker_id = Id::new();
        let result = book.match_market_order_with_user(taker_id, 10, Side::Buy, same_user);
        assert!(result.is_ok());
        let mr = result.unwrap();
        assert!(mr.is_complete());
        assert_eq!(mr.executed_quantity().unwrap(), Quantity::new(10));
    }

    // -----------------------------------------------------------------------
    // STPMode::CancelTaker
    // -----------------------------------------------------------------------

    #[test]
    fn test_cancel_taker_prevents_self_trade() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelTaker);

        let same_user = user(1);
        let maker_id = add_sell_order_with_user(&book, 100, 10, same_user);

        // Same user tries to buy — STP should block
        let taker_id = Id::new();
        let result = book.match_market_order_with_user(taker_id, 10, Side::Buy, same_user);

        match result {
            Err(OrderBookError::SelfTradePrevented { mode, .. }) => {
                assert_eq!(mode, STPMode::CancelTaker);
            }
            other => panic!("expected SelfTradePrevented, got {other:?}"),
        }

        // Maker order should still be in the book
        assert!(book.get_order(maker_id).is_some());
        assert_eq!(book.best_ask(), Some(100));
    }

    #[test]
    fn test_cancel_taker_allows_different_users() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelTaker);

        let maker_user = user(1);
        let taker_user = user(2);
        add_sell_order_with_user(&book, 100, 10, maker_user);

        // Different user buys — should match normally
        let taker_id = Id::new();
        let result = book.match_market_order_with_user(taker_id, 10, Side::Buy, taker_user);
        assert!(result.is_ok());
        let mr = result.unwrap();
        assert!(mr.is_complete());
        assert_eq!(mr.executed_quantity().unwrap(), Quantity::new(10));
    }

    #[test]
    fn test_cancel_taker_partial_fill_before_self_trade() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelTaker);

        let taker_user = user(1);
        let other_user = user(2);

        // Use different price levels to guarantee execution order:
        // Level 100: other user (qty 5) — matched first (best ask)
        // Level 200: same user (qty 10) — STP triggers here
        add_sell_order_with_user(&book, 100, 5, other_user);
        add_sell_order_with_user(&book, 200, 10, taker_user);

        // Taker tries to buy 20 — should fill 5 at price 100, then STP at 200
        let taker_id = Id::new();
        let result = book.match_market_order_with_user(taker_id, 20, Side::Buy, taker_user);

        // Should succeed with partial fill (STP only returns error when zero fills)
        assert!(result.is_ok(), "expected Ok, got: {result:?}");
        let mr = result.unwrap();
        assert_eq!(mr.executed_quantity().unwrap(), Quantity::new(5));
        assert!(!mr.is_complete());
    }

    #[test]
    fn test_cancel_taker_zero_taker_user_bypasses_stp_during_matching() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelTaker);

        let maker_user = user(1);
        add_sell_order_with_user(&book, 100, 10, maker_user);

        // Matching with Hash32::zero() as taker_user_id should bypass STP
        let taker_id = Id::new();
        let result = book.match_market_order_with_user(taker_id, 10, Side::Buy, Hash32::zero());
        assert!(result.is_ok());
        let mr = result.unwrap();
        assert!(mr.is_complete());
    }

    // -----------------------------------------------------------------------
    // STPMode::CancelMaker
    // -----------------------------------------------------------------------

    #[test]
    fn test_cancel_maker_removes_same_user_orders() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelMaker);

        let same_user = user(1);
        let other_user = user(2);

        // Same user resting sell at 100 (qty 5)
        let maker_id = add_sell_order_with_user(&book, 100, 5, same_user);
        // Other user resting sell at 100 (qty 10)
        add_sell_order_with_user(&book, 100, 10, other_user);

        // Same user buys 10 — maker should be cancelled, match against other
        let taker_id = Id::new();
        let result = book.match_market_order_with_user(taker_id, 10, Side::Buy, same_user);
        assert!(result.is_ok());
        let mr = result.unwrap();
        assert_eq!(mr.executed_quantity().unwrap(), Quantity::new(10));
        assert!(mr.is_complete());

        // Same user's maker order should be gone
        assert!(book.get_order(maker_id).is_none());
    }

    #[test]
    fn test_cancel_maker_all_same_user_orders_cancelled() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelMaker);

        let same_user = user(1);

        // Only same-user orders at this level
        let maker_id1 = add_sell_order_with_user(&book, 100, 5, same_user);
        let maker_id2 = add_sell_order_with_user(&book, 100, 3, same_user);

        // Taker tries to buy — all makers cancelled, level empty, no fills
        let taker_id = Id::new();
        let result = book.match_market_order_with_user(taker_id, 10, Side::Buy, same_user);

        // Market order with no liquidity after cancellations returns InsufficientLiquidity
        match result {
            Err(OrderBookError::InsufficientLiquidity { .. }) => {}
            other => panic!("expected InsufficientLiquidity, got {other:?}"),
        }

        // Both makers should be gone
        assert!(book.get_order(maker_id1).is_none());
        assert!(book.get_order(maker_id2).is_none());
        assert_eq!(book.best_ask(), None);
    }

    #[test]
    fn test_cancel_maker_across_price_levels() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelMaker);

        let same_user = user(1);
        let other_user = user(2);

        // Level 100: same user (qty 5)
        let maker1 = add_sell_order_with_user(&book, 100, 5, same_user);
        // Level 200: other user (qty 10)
        add_sell_order_with_user(&book, 200, 10, other_user);

        // Buy 10 — cancel maker at 100, then match at 200
        let taker_id = Id::new();
        let result = book.match_market_order_with_user(taker_id, 10, Side::Buy, same_user);
        assert!(result.is_ok());
        let mr = result.unwrap();
        assert_eq!(mr.executed_quantity().unwrap(), Quantity::new(10));

        // Maker at 100 should be gone
        assert!(book.get_order(maker1).is_none());
    }

    // -----------------------------------------------------------------------
    // STPMode::CancelBoth
    // -----------------------------------------------------------------------

    #[test]
    fn test_cancel_both_cancels_maker_and_taker() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelBoth);

        let same_user = user(1);
        let maker_id = add_sell_order_with_user(&book, 100, 10, same_user);

        // Same user tries to buy — both should be cancelled
        let taker_id = Id::new();
        let result = book.match_market_order_with_user(taker_id, 10, Side::Buy, same_user);

        match result {
            Err(OrderBookError::SelfTradePrevented { mode, .. }) => {
                assert_eq!(mode, STPMode::CancelBoth);
            }
            other => panic!("expected SelfTradePrevented, got {other:?}"),
        }

        // Maker should be cancelled too
        assert!(book.get_order(maker_id).is_none());
    }

    #[test]
    fn test_cancel_both_partial_fill_before_self() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelBoth);

        let taker_user = user(1);
        let other_user = user(2);

        // Use different price levels to guarantee execution order:
        // Level 100: other user (qty 3) — matched first (best ask)
        // Level 200: same user (qty 10) — CancelBoth triggers here
        add_sell_order_with_user(&book, 100, 3, other_user);
        let maker_id = add_sell_order_with_user(&book, 200, 10, taker_user);

        // Taker buys 20 — fills 3 at 100, then CancelBoth at 200
        let taker_id = Id::new();
        let result = book.match_market_order_with_user(taker_id, 20, Side::Buy, taker_user);

        // Partial fill occurred, so result is Ok (not error)
        assert!(result.is_ok());
        let mr = result.unwrap();
        assert_eq!(mr.executed_quantity().unwrap(), Quantity::new(3));
        assert!(!mr.is_complete());

        // Same-user maker should be cancelled
        assert!(book.get_order(maker_id).is_none());
    }

    // -----------------------------------------------------------------------
    // STP with add_order (limit order crossing)
    // -----------------------------------------------------------------------

    #[test]
    fn test_stp_cancel_taker_via_add_order() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelTaker);

        let same_user = user(1);
        add_sell_order_with_user(&book, 100, 10, same_user);

        // Same user adds an aggressive buy that would cross
        let order = OrderType::Standard {
            id: Id::new(),
            price: Price::new(100),
            quantity: Quantity::new(10),
            side: Side::Buy,
            user_id: same_user,
            timestamp: TimestampMs::new(crate::utils::current_time_millis()),
            time_in_force: TimeInForce::Gtc,
            extra_fields: (),
        };

        let result = book.add_order(order);
        match result {
            Err(OrderBookError::SelfTradePrevented { mode, .. }) => {
                assert_eq!(mode, STPMode::CancelTaker);
            }
            other => panic!("expected SelfTradePrevented, got {other:?}"),
        }

        // Ask side unchanged
        assert_eq!(book.best_ask(), Some(100));
    }

    #[test]
    fn test_stp_cancel_maker_via_add_order() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelMaker);

        let same_user = user(1);
        let other_user = user(2);

        // Same user sell at 100 (qty 5)
        let maker_id = add_sell_order_with_user(&book, 100, 5, same_user);
        // Other user sell at 100 (qty 10)
        add_sell_order_with_user(&book, 100, 10, other_user);

        // Same user adds aggressive buy at 100 for qty 8
        let order = OrderType::Standard {
            id: Id::new(),
            price: Price::new(100),
            quantity: Quantity::new(8),
            side: Side::Buy,
            user_id: same_user,
            timestamp: TimestampMs::new(crate::utils::current_time_millis()),
            time_in_force: TimeInForce::Gtc,
            extra_fields: (),
        };

        let result = book.add_order(order);
        assert!(result.is_ok());

        // Same-user maker should be gone
        assert!(book.get_order(maker_id).is_none());
    }

    // -----------------------------------------------------------------------
    // Sell-side STP (taker selling into bids)
    // -----------------------------------------------------------------------

    #[test]
    fn test_stp_cancel_taker_sell_side() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelTaker);

        let same_user = user(1);
        let maker_id = add_buy_order_with_user(&book, 100, 10, same_user);

        // Same user tries to sell — STP should block
        let taker_id = Id::new();
        let result = book.match_market_order_with_user(taker_id, 10, Side::Sell, same_user);

        match result {
            Err(OrderBookError::SelfTradePrevented { mode, .. }) => {
                assert_eq!(mode, STPMode::CancelTaker);
            }
            other => panic!("expected SelfTradePrevented, got {other:?}"),
        }

        // Maker should still be in the book
        assert!(book.get_order(maker_id).is_some());
        assert_eq!(book.best_bid(), Some(100));
    }

    #[test]
    fn test_stp_cancel_maker_sell_side() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelMaker);

        let same_user = user(1);
        let other_user = user(2);

        // Same user buy at 200 (qty 5)
        let maker_id = add_buy_order_with_user(&book, 200, 5, same_user);
        // Other user buy at 200 (qty 10)
        add_buy_order_with_user(&book, 200, 10, other_user);

        // Same user sells 10 — maker cancelled, match against other
        let taker_id = Id::new();
        let result = book.match_market_order_with_user(taker_id, 10, Side::Sell, same_user);
        assert!(result.is_ok());
        let mr = result.unwrap();
        assert_eq!(mr.executed_quantity().unwrap(), Quantity::new(10));

        // Same user maker gone
        assert!(book.get_order(maker_id).is_none());
    }

    // -----------------------------------------------------------------------
    // Setter / getter / constructor
    // -----------------------------------------------------------------------

    #[test]
    fn test_stp_mode_setter_getter() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        assert_eq!(book.stp_mode(), STPMode::None);

        book.set_stp_mode(STPMode::CancelTaker);
        assert_eq!(book.stp_mode(), STPMode::CancelTaker);

        book.set_stp_mode(STPMode::CancelMaker);
        assert_eq!(book.stp_mode(), STPMode::CancelMaker);

        book.set_stp_mode(STPMode::CancelBoth);
        assert_eq!(book.stp_mode(), STPMode::CancelBoth);

        book.set_stp_mode(STPMode::None);
        assert_eq!(book.stp_mode(), STPMode::None);
    }

    #[test]
    fn test_with_stp_mode_constructor() {
        let book: OrderBook<()> = OrderBook::with_stp_mode("TEST", STPMode::CancelBoth);
        assert_eq!(book.stp_mode(), STPMode::CancelBoth);
        assert_eq!(book.symbol(), "TEST");
    }

    // -----------------------------------------------------------------------
    // Edge cases
    // -----------------------------------------------------------------------

    #[test]
    fn test_stp_empty_book_returns_insufficient_liquidity() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelTaker);

        let taker_id = Id::new();
        let result = book.match_market_order_with_user(taker_id, 10, Side::Buy, user(1));

        match result {
            Err(OrderBookError::InsufficientLiquidity { .. }) => {}
            other => panic!("expected InsufficientLiquidity, got {other:?}"),
        }
    }

    #[test]
    fn test_stp_limit_order_no_cross_adds_to_book() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelTaker);

        let same_user = user(1);
        add_sell_order_with_user(&book, 200, 10, same_user);

        // Same user adds buy at 100 (no cross) — should rest in book
        let order = OrderType::Standard {
            id: Id::new(),
            price: Price::new(100),
            quantity: Quantity::new(5),
            side: Side::Buy,
            user_id: same_user,
            timestamp: TimestampMs::new(crate::utils::current_time_millis()),
            time_in_force: TimeInForce::Gtc,
            extra_fields: (),
        };

        let result = book.add_order(order);
        assert!(result.is_ok());
        assert_eq!(book.best_bid(), Some(100));
    }

    #[test]
    fn test_stp_submit_market_order_with_user() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelTaker);

        let same_user = user(1);
        add_sell_order_with_user(&book, 100, 10, same_user);

        // Use the submit_market_order_with_user convenience method
        let taker_id = Id::new();
        let result = book.submit_market_order_with_user(taker_id, 10, Side::Buy, same_user);

        match result {
            Err(OrderBookError::SelfTradePrevented { mode, .. }) => {
                assert_eq!(mode, STPMode::CancelTaker);
            }
            other => panic!("expected SelfTradePrevented, got {other:?}"),
        }
    }

    #[test]
    fn test_stp_match_limit_order_with_user() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelTaker);

        let same_user = user(1);
        add_sell_order_with_user(&book, 100, 10, same_user);

        // Use match_limit_order_with_user
        let taker_id = Id::new();
        let result = book.match_limit_order_with_user(taker_id, 10, Side::Buy, 100, same_user);

        match result {
            Err(OrderBookError::SelfTradePrevented { mode, .. }) => {
                assert_eq!(mode, STPMode::CancelTaker);
            }
            other => panic!("expected SelfTradePrevented, got {other:?}"),
        }
    }

    #[test]
    fn test_stp_backward_compat_no_user_id() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelTaker);

        let same_user = user(1);
        add_sell_order_with_user(&book, 100, 10, same_user);

        // Using the old API (no user_id) should bypass STP
        let taker_id = Id::new();
        let result = book.match_market_order(taker_id, 10, Side::Buy);
        assert!(result.is_ok());
        let mr = result.unwrap();
        assert!(mr.is_complete());
    }

    #[test]
    fn test_stp_multiple_levels_cancel_taker() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelTaker);

        let taker_user = user(1);
        let other_user = user(2);

        // Level 100: other user (qty 5)
        add_sell_order_with_user(&book, 100, 5, other_user);
        // Level 200: same user (qty 10)
        add_sell_order_with_user(&book, 200, 10, taker_user);

        // Buy 20 — fills 5 at 100, then STP at 200
        let taker_id = Id::new();
        let result = book.match_market_order_with_user(taker_id, 20, Side::Buy, taker_user);

        assert!(result.is_ok());
        let mr = result.unwrap();
        assert_eq!(mr.executed_quantity().unwrap(), Quantity::new(5));
        assert!(!mr.is_complete());
    }

    // -----------------------------------------------------------------------
    // MissingUserId enforcement (issue #9)
    // -----------------------------------------------------------------------

    #[test]
    fn test_missing_user_id_limit_order_stp_enabled() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelTaker);

        // add_limit_order defaults to Hash32::zero() → should be rejected
        let result = book.add_limit_order(Id::new(), 100, 10, Side::Buy, TimeInForce::Gtc, None);
        match result {
            Err(OrderBookError::MissingUserId { .. }) => {}
            other => panic!("expected MissingUserId, got {other:?}"),
        }
    }

    #[test]
    fn test_missing_user_id_limit_order_stp_disabled() {
        let book: OrderBook<()> = OrderBook::new("TEST");
        assert_eq!(book.stp_mode(), STPMode::None);

        // STP disabled → Hash32::zero() is fine
        let result = book.add_limit_order(Id::new(), 100, 10, Side::Buy, TimeInForce::Gtc, None);
        assert!(result.is_ok());
    }

    #[test]
    fn test_limit_order_with_user_stp_enabled() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelTaker);

        // Non-zero user_id → accepted
        let result = book.add_limit_order_with_user(
            Id::new(),
            100,
            10,
            Side::Buy,
            TimeInForce::Gtc,
            user(1),
            None,
        );
        assert!(result.is_ok());
    }

    #[test]
    fn test_limit_order_with_zero_user_stp_enabled() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelMaker);

        // Explicitly zero user_id via _with_user → should be rejected
        let result = book.add_limit_order_with_user(
            Id::new(),
            100,
            10,
            Side::Buy,
            TimeInForce::Gtc,
            Hash32::zero(),
            None,
        );
        match result {
            Err(OrderBookError::MissingUserId { .. }) => {}
            other => panic!("expected MissingUserId, got {other:?}"),
        }
    }

    #[test]
    fn test_missing_user_id_iceberg_order_stp_enabled() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelBoth);

        let result =
            book.add_iceberg_order(Id::new(), 100, 5, 10, Side::Sell, TimeInForce::Gtc, None);
        match result {
            Err(OrderBookError::MissingUserId { .. }) => {}
            other => panic!("expected MissingUserId, got {other:?}"),
        }
    }

    #[test]
    fn test_iceberg_order_with_user_stp_enabled() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelTaker);

        let result = book.add_iceberg_order_with_user(
            Id::new(),
            100,
            5,
            10,
            Side::Sell,
            TimeInForce::Gtc,
            user(2),
            None,
        );
        assert!(result.is_ok());
    }

    #[test]
    fn test_iceberg_order_stp_disabled() {
        let book: OrderBook<()> = OrderBook::new("TEST");

        let result =
            book.add_iceberg_order(Id::new(), 100, 5, 10, Side::Sell, TimeInForce::Gtc, None);
        assert!(result.is_ok());
    }

    #[test]
    fn test_missing_user_id_post_only_order_stp_enabled() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelTaker);

        let result =
            book.add_post_only_order(Id::new(), 100, 10, Side::Buy, TimeInForce::Gtc, None);
        match result {
            Err(OrderBookError::MissingUserId { .. }) => {}
            other => panic!("expected MissingUserId, got {other:?}"),
        }
    }

    #[test]
    fn test_post_only_order_with_user_stp_enabled() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelMaker);

        let result = book.add_post_only_order_with_user(
            Id::new(),
            200,
            10,
            Side::Sell,
            TimeInForce::Gtc,
            user(3),
            None,
        );
        assert!(result.is_ok());
    }

    #[test]
    fn test_post_only_order_stp_disabled() {
        let book: OrderBook<()> = OrderBook::new("TEST");

        let result =
            book.add_post_only_order(Id::new(), 100, 10, Side::Buy, TimeInForce::Gtc, None);
        assert!(result.is_ok());
    }

    #[test]
    fn test_add_order_direct_zero_user_stp_enabled() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelTaker);

        // Direct add_order with zero user_id → should be rejected
        let order = OrderType::Standard {
            id: Id::new(),
            price: Price::new(100),
            quantity: Quantity::new(10),
            side: Side::Buy,
            user_id: Hash32::zero(),
            timestamp: TimestampMs::new(crate::utils::current_time_millis()),
            time_in_force: TimeInForce::Gtc,
            extra_fields: (),
        };
        let result = book.add_order(order);
        match result {
            Err(OrderBookError::MissingUserId { .. }) => {}
            other => panic!("expected MissingUserId, got {other:?}"),
        }
    }

    #[test]
    fn test_add_order_direct_nonzero_user_stp_enabled() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelBoth);

        // Direct add_order with non-zero user_id → should be accepted
        let order = OrderType::Standard {
            id: Id::new(),
            price: Price::new(100),
            quantity: Quantity::new(10),
            side: Side::Buy,
            user_id: user(5),
            timestamp: TimestampMs::new(crate::utils::current_time_millis()),
            time_in_force: TimeInForce::Gtc,
            extra_fields: (),
        };
        let result = book.add_order(order);
        assert!(result.is_ok());
    }

    #[test]
    fn test_missing_user_id_all_stp_modes() {
        // Verify enforcement for every non-None STP mode
        for mode in [
            STPMode::CancelTaker,
            STPMode::CancelMaker,
            STPMode::CancelBoth,
        ] {
            let mut book: OrderBook<()> = OrderBook::new("TEST");
            book.set_stp_mode(mode);

            let result =
                book.add_limit_order(Id::new(), 100, 10, Side::Buy, TimeInForce::Gtc, None);
            match result {
                Err(OrderBookError::MissingUserId { .. }) => {}
                other => panic!("expected MissingUserId for mode {mode}, got {other:?}"),
            }
        }
    }

    #[test]
    fn test_missing_user_id_error_contains_order_id() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelTaker);

        let oid = Id::new();
        let order = OrderType::Standard {
            id: oid,
            price: Price::new(100),
            quantity: Quantity::new(10),
            side: Side::Buy,
            user_id: Hash32::zero(),
            timestamp: TimestampMs::new(crate::utils::current_time_millis()),
            time_in_force: TimeInForce::Gtc,
            extra_fields: (),
        };
        let result = book.add_order(order);
        match result {
            Err(OrderBookError::MissingUserId { order_id }) => {
                assert_eq!(order_id, oid);
            }
            other => panic!("expected MissingUserId, got {other:?}"),
        }
    }

    #[test]
    fn test_missing_user_id_display() {
        let oid = Id::new();
        let err = OrderBookError::MissingUserId { order_id: oid };
        let msg = err.to_string();
        assert!(msg.contains("missing user_id"));
        assert!(msg.contains("STP"));
    }

    // -----------------------------------------------------------------------
    // #168 — STP self-cross modify atomicity: a re-price that would self-cross
    // the same user's opposite-side liquidity under CancelTaker / CancelBoth is
    // rejected BEFORE the original is cancelled, so the original survives.
    // -----------------------------------------------------------------------

    /// CancelTaker: re-pricing a buy up into the same user's own resting sell is
    /// rejected pre-cancel with `SelfTradePrevented`; the original buy survives
    /// unchanged and the opposite-side maker is untouched.
    #[test]
    fn test_modify_self_cross_under_cancel_taker_rejected_preserves_original_issue_168() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelTaker);
        let u = user(7);

        let buy_id = add_buy_order_with_user(&book, 100, 10, u);
        let sell_id = add_sell_order_with_user(&book, 110, 10, u);

        // Re-price the buy up to 110 — it would cross the user's own sell.
        let res = book.update_order(OrderUpdate::UpdatePrice {
            order_id: buy_id,
            new_price: Price::new(110),
        });
        assert!(
            matches!(
                res,
                Err(OrderBookError::SelfTradePrevented { taker_order_id, .. }) if taker_order_id == buy_id
            ),
            "self-crossing modify must be STP-rejected pre-cancel, got {res:?}"
        );

        // The original buy survives at its ORIGINAL price, and the sell is intact.
        let buy = book.get_order(buy_id).expect("original buy must survive");
        assert_eq!(
            buy.price().as_u128(),
            100,
            "original price must be unchanged"
        );
        assert!(
            book.get_order(sell_id).is_some(),
            "the maker must be untouched"
        );
        assert!(
            !book.has_traded.load(std::sync::atomic::Ordering::SeqCst),
            "a rejected modify must emit no trades"
        );
    }

    /// CancelBoth: same self-cross scenario is rejected pre-cancel (the taker is
    /// cancelled under CancelBoth too), preserving the original.
    #[test]
    fn test_modify_self_cross_under_cancel_both_rejected_preserves_original_issue_168() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelBoth);
        let u = user(8);

        let sell_id = add_sell_order_with_user(&book, 110, 10, u);
        let bid_maker = add_buy_order_with_user(&book, 90, 10, u);

        // Re-price the sell down to 90 — it would cross the user's own bid.
        let res = book.update_order(OrderUpdate::UpdatePrice {
            order_id: sell_id,
            new_price: Price::new(90),
        });
        assert!(
            matches!(res, Err(OrderBookError::SelfTradePrevented { .. })),
            "self-crossing modify under CancelBoth must be rejected pre-cancel, got {res:?}"
        );
        let sell = book.get_order(sell_id).expect("original sell must survive");
        assert_eq!(sell.price().as_u128(), 110, "original price unchanged");
        assert!(
            book.get_order(bid_maker).is_some(),
            "the bid maker untouched"
        );
    }

    /// CancelMaker: the taker is rested (the maker is cancelled), so the modify
    /// must NOT be pre-rejected — it succeeds, re-pricing the order and letting
    /// STP cancel the same-user maker. Confirms no spurious pre-rejection.
    #[test]
    fn test_modify_self_cross_under_cancel_maker_not_prerejected_issue_168() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelMaker);
        let u = user(9);

        let buy_id = add_buy_order_with_user(&book, 100, 10, u);
        let sell_id = add_sell_order_with_user(&book, 110, 10, u);

        let res = book.update_order(OrderUpdate::UpdatePrice {
            order_id: buy_id,
            new_price: Price::new(110),
        });
        assert!(
            res.is_ok(),
            "CancelMaker modify must not be pre-rejected, got {res:?}"
        );
        // The re-priced buy rests; the same-user sell maker is STP-cancelled.
        assert!(
            book.get_order(buy_id).is_some(),
            "the re-priced taker rests"
        );
        assert!(
            book.get_order(sell_id).is_none(),
            "the same-user maker is cancelled by CancelMaker"
        );
    }

    /// CancelTaker, different user: re-pricing into ANOTHER user's resting order
    /// is a normal cross, not a self-cross, so it must NOT be pre-rejected.
    #[test]
    fn test_modify_cross_other_user_under_cancel_taker_allowed_issue_168() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelTaker);
        let u = user(1);
        let other = user(2);

        let buy_id = add_buy_order_with_user(&book, 100, 10, u);
        let _other_sell = add_sell_order_with_user(&book, 110, 10, other);

        let res = book.update_order(OrderUpdate::UpdatePrice {
            order_id: buy_id,
            new_price: Price::new(110),
        });
        assert!(
            res.is_ok(),
            "crossing another user's order is not a self-cross, must succeed, got {res:?}"
        );
        assert!(
            book.has_traded.load(std::sync::atomic::Ordering::SeqCst),
            "the re-priced buy fills against the other user's sell"
        );
    }

    /// CancelTaker, precise quantity accounting: if the taker fully fills against
    /// strictly-higher-priority NON-self depth before the sweep reaches the
    /// same-user maker, the engine never cancels it — so the modify must NOT be
    /// pre-rejected. Pins that the check accounts for fillable depth, not just
    /// the existence of a same-user maker in range.
    #[test]
    fn test_modify_self_cross_taker_fills_before_self_maker_allowed_issue_168() {
        let mut book: OrderBook<()> = OrderBook::new("TEST");
        book.set_stp_mode(STPMode::CancelTaker);
        let u = user(3);
        let other = user(4);

        // Buy 5 at 100. Opposite asks: other-user 10 @ 105 (better), same-user 10 @ 110.
        let buy_id = add_buy_order_with_user(&book, 100, 5, u);
        let _other_sell = add_sell_order_with_user(&book, 105, 10, other);
        let self_sell = add_sell_order_with_user(&book, 110, 10, u);

        // Re-price to 110: the buy (qty 5) fully fills against the other-user
        // 105 ask and never reaches the same-user 110 ask, so no STP cancel.
        let res = book.update_order(OrderUpdate::UpdatePrice {
            order_id: buy_id,
            new_price: Price::new(110),
        });
        assert!(
            res.is_ok(),
            "taker satisfied before the same-user maker must not be rejected, got {res:?}"
        );
        assert!(book.has_traded.load(std::sync::atomic::Ordering::SeqCst));
        // The same-user sell at 110 is untouched (never reached).
        assert!(
            book.get_order(self_sell).is_some(),
            "the same-user maker beyond the fill point is untouched"
        );
    }

    /// CancelTaker with `lot_size` set: the self-cross verdict still matches the
    /// engine. `validate_order_shape` (which enforces lot-alignment) runs before
    /// the pre-check, so every quantity is a lot multiple and the engine's
    /// per-level `level_qty_cap` lot-rounding is a no-op — the helper's
    /// `matchable_quantity` consumption and the engine's sweep agree, leaving no
    /// sub-lot dust that could strand a same-user maker. Pins that invariant.
    #[test]
    fn test_modify_self_cross_under_cancel_taker_with_lot_size_rejected_issue_168() {
        let mut book: OrderBook<()> = OrderBook::with_lot_size("TEST", 5);
        book.set_stp_mode(STPMode::CancelTaker);
        let u = user(5);
        let other = user(6);

        // Buy 10 (lot-aligned) at 100. Opposite asks: other-user 5 @ 105,
        // same-user 10 @ 110. Re-pricing to 110 fills 5 against the other-user
        // ask (remaining 5) then reaches the same-user 110 ask with remaining>0
        // → the engine cancels the taker, so the pre-check must reject.
        let buy_id = add_buy_order_with_user(&book, 100, 10, u);
        let _other_sell = add_sell_order_with_user(&book, 105, 5, other);
        let self_sell = add_sell_order_with_user(&book, 110, 10, u);

        let res = book.update_order(OrderUpdate::UpdatePrice {
            order_id: buy_id,
            new_price: Price::new(110),
        });
        assert!(
            matches!(res, Err(OrderBookError::SelfTradePrevented { .. })),
            "lot-sized self-cross must be rejected pre-cancel, got {res:?}"
        );
        let buy = book.get_order(buy_id).expect("original buy survives");
        assert_eq!(buy.price().as_u128(), 100, "original price unchanged");
        assert!(
            book.get_order(self_sell).is_some(),
            "same-user maker untouched"
        );
        assert!(
            !book.has_traded.load(std::sync::atomic::Ordering::SeqCst),
            "a rejected modify emits no trades"
        );
    }
}
