"""
OrderBookクラスのユニットテスト

LOB再構築のadd/modify/cancel処理テスト。
"""

import pytest

from rithmic_bridge.models import MBOEvent, MBOAction, Side
from rithmic_bridge.client.order_book import OrderBook


class TestOrderBookBasic:
    """OrderBookの基本機能テスト"""

    def setup_method(self):
        """各テストの前にOrderBookを初期化"""
        self.book = OrderBook(symbol="GCJ6.COMEX")

    def test_add_bid_order(self):
        """Bid注文の追加が正しく動作すること"""
        event = MBOEvent(
            symbol="GCJ6.COMEX",
            action=MBOAction.ADD,
            order_id="1",
            side=Side.BID,
            price=2500.0,
            size=5,
            timestamp=1000.0,
        )
        self.book.process_event(event)

        assert self.book.order_count == 1
        assert self.book.bid_level_count == 1
        assert self.book.ask_level_count == 0

        bids = self.book.get_bids()
        assert len(bids) == 1
        assert bids[0] == (2500.0, 5, 1)  # (price, total_size, order_count)

    def test_add_ask_order(self):
        """Ask注文の追加が正しく動作すること"""
        event = MBOEvent(
            symbol="GCJ6.COMEX",
            action=MBOAction.ADD,
            order_id="1",
            side=Side.ASK,
            price=2501.0,
            size=3,
            timestamp=1000.0,
        )
        self.book.process_event(event)

        assert self.book.order_count == 1
        assert self.book.ask_level_count == 1

        asks = self.book.get_asks()
        assert len(asks) == 1
        assert asks[0] == (2501.0, 3, 1)

    def test_add_multiple_orders_same_price(self):
        """同一価格での複数注文が正しく集約されること"""
        # 同じ価格に2つの買い注文
        self.book.process_event(MBOEvent(
            symbol="GCJ6.COMEX", action=MBOAction.ADD,
            order_id="1", side=Side.BID, price=2500.0, size=5, timestamp=1000.0,
        ))
        self.book.process_event(MBOEvent(
            symbol="GCJ6.COMEX", action=MBOAction.ADD,
            order_id="2", side=Side.BID, price=2500.0, size=3, timestamp=1001.0,
        ))

        assert self.book.order_count == 2
        assert self.book.bid_level_count == 1  # 価格レベルは1つ

        bids = self.book.get_bids()
        assert bids[0] == (2500.0, 8, 2)  # 合計サイズ5+3=8、注文数2

    def test_modify_order(self):
        """注文の変更が正しく動作すること"""
        # 追加
        self.book.process_event(MBOEvent(
            symbol="GCJ6.COMEX", action=MBOAction.ADD,
            order_id="1", side=Side.BID, price=2500.0, size=5, timestamp=1000.0,
        ))
        # 変更（サイズ3に変更）
        self.book.process_event(MBOEvent(
            symbol="GCJ6.COMEX", action=MBOAction.MODIFY,
            order_id="1", side=Side.BID, price=2500.0, size=3, timestamp=1001.0,
        ))

        assert self.book.order_count == 1
        bids = self.book.get_bids()
        assert bids[0] == (2500.0, 3, 1)  # サイズが3に変更

    def test_cancel_order(self):
        """注文のキャンセルが正しく動作すること"""
        # 追加
        self.book.process_event(MBOEvent(
            symbol="GCJ6.COMEX", action=MBOAction.ADD,
            order_id="1", side=Side.BID, price=2500.0, size=5, timestamp=1000.0,
        ))
        # キャンセル
        self.book.process_event(MBOEvent(
            symbol="GCJ6.COMEX", action=MBOAction.CANCEL,
            order_id="1", side=Side.BID, price=2500.0, size=5, timestamp=1001.0,
        ))

        assert self.book.order_count == 0
        assert self.book.bid_level_count == 0

    def test_cancel_unknown_order(self):
        """不明なIDのキャンセルが安全に処理されること"""
        self.book.process_event(MBOEvent(
            symbol="GCJ6.COMEX", action=MBOAction.CANCEL,
            order_id="unknown", side=Side.BID, price=2500.0, size=5, timestamp=1000.0,
        ))
        assert self.book.order_count == 0  # エラーにならない


class TestOrderBookMetrics:
    """OrderBookのメトリクスのテスト"""

    def setup_method(self):
        self.book = OrderBook(symbol="GCJ6.COMEX")
        # 板を構築
        self._build_sample_book()

    def _build_sample_book(self):
        """テスト用のサンプル板を構築"""
        # Bid側
        for i, (price, size) in enumerate([
            (2500.0, 10), (2499.9, 8), (2499.8, 5),
        ]):
            self.book.process_event(MBOEvent(
                symbol="GCJ6.COMEX", action=MBOAction.ADD,
                order_id=f"b{i}", side=Side.BID,
                price=price, size=size, timestamp=1000.0 + i,
            ))
        # Ask側
        for i, (price, size) in enumerate([
            (2500.1, 7), (2500.2, 12), (2500.3, 3),
        ]):
            self.book.process_event(MBOEvent(
                symbol="GCJ6.COMEX", action=MBOAction.ADD,
                order_id=f"a{i}", side=Side.ASK,
                price=price, size=size, timestamp=1003.0 + i,
            ))

    def test_best_bid(self):
        """最良Bidが正しいこと"""
        best = self.book.get_best_bid()
        assert best == (2500.0, 10)

    def test_best_ask(self):
        """最良Askが正しいこと"""
        best = self.book.get_best_ask()
        assert best == (2500.1, 7)

    def test_mid_price(self):
        """仲値が正しいこと"""
        mid = self.book.get_mid_price()
        assert mid == pytest.approx(2500.05, abs=0.01)

    def test_spread(self):
        """スプレッドが正しいこと"""
        spread = self.book.get_spread()
        assert spread == pytest.approx(0.1, abs=0.01)

    def test_bid_ordering(self):
        """Bidが価格降順で返ること"""
        bids = self.book.get_bids(3)
        prices = [b[0] for b in bids]
        assert prices == sorted(prices, reverse=True)

    def test_ask_ordering(self):
        """Askが価格昇順で返ること"""
        asks = self.book.get_asks(3)
        prices = [a[0] for a in asks]
        assert prices == sorted(prices)

    def test_total_bid_volume(self):
        """Bid合計数量が正しいこと"""
        vol = self.book.get_total_bid_volume(3)
        assert vol == 10 + 8 + 5  # = 23

    def test_total_ask_volume(self):
        """Ask合計数量が正しいこと"""
        vol = self.book.get_total_ask_volume(3)
        assert vol == 7 + 12 + 3  # = 22

    def test_book_imbalance(self):
        """インバランスが正しいこと"""
        imb = self.book.get_book_imbalance(3)
        bid_vol = 23
        ask_vol = 22
        expected = (bid_vol - ask_vol) / (bid_vol + ask_vol)
        assert imb == pytest.approx(expected, abs=0.001)

    def test_clear(self):
        """クリアが正しく動作すること"""
        self.book.clear()
        assert self.book.order_count == 0
        assert self.book.bid_level_count == 0
        assert self.book.ask_level_count == 0
        assert self.book.get_best_bid() is None
        assert self.book.get_best_ask() is None


class TestOrderBookNumpy:
    """OrderBookのNumpy変換テスト"""

    def test_to_numpy(self):
        """to_numpyが正しい形状の配列を返すこと"""
        try:
            import numpy as np
        except ImportError:
            pytest.skip("numpyが利用不可")

        book = OrderBook(symbol="GCJ6.COMEX")
        book.process_event(MBOEvent(
            symbol="GCJ6.COMEX", action=MBOAction.ADD,
            order_id="1", side=Side.BID, price=2500.0, size=5, timestamp=1000.0,
        ))
        book.process_event(MBOEvent(
            symbol="GCJ6.COMEX", action=MBOAction.ADD,
            order_id="2", side=Side.ASK, price=2500.1, size=3, timestamp=1001.0,
        ))

        n_levels = 5
        arr = book.to_numpy(n_levels=n_levels)
        assert arr is not None
        assert arr.shape == (n_levels * 6 + 3,)
        assert arr.dtype == np.float32
