"""
Comprehensive test suite for orderbook detection module.

Tests the OrderDetection class which provides advanced order detection
capabilities for identifying iceberg orders, spoofing patterns, and other
market manipulation techniques.

Author: @TexasCoding
Date: 2025-01-27
"""

import asyncio
from collections import deque
from datetime import UTC, datetime, timedelta
from unittest.mock import AsyncMock, MagicMock, patch

import polars as pl
import pytest

from project_x_py.orderbook.base import OrderBookBase
from project_x_py.orderbook.detection import OrderDetection


@pytest.fixture
def mock_orderbook_base():
    """Create a mock OrderBookBase with test data."""
    ob = MagicMock(spec=OrderBookBase)
    ob.timezone = UTC
    ob.orderbook_lock = asyncio.Lock()
    ob.instrument = "MNQ"

    # Set up test orderbook data
    ob.orderbook_bids = pl.DataFrame({
        "price": [21000.0, 20999.0, 20998.0, 20997.0, 20996.0],
        "volume": [10, 200, 15, 25, 30],  # Note: large volume at 20999
        "timestamp": [datetime.now(UTC)] * 5,
    })

    ob.orderbook_asks = pl.DataFrame({
        "price": [21001.0, 21002.0, 21003.0, 21004.0, 21005.0],
        "volume": [15, 25, 300, 30, 10],  # Note: large volume at 21003
        "timestamp": [datetime.now(UTC)] * 5,
    })

    # Set up recent trades for detection
    ob.recent_trades = pl.DataFrame({
        "price": [21000.5] * 20,  # 20 trades at same price (potential iceberg)
        "volume": [10] * 20,       # Consistent small size
        "timestamp": [datetime.now(UTC) - timedelta(seconds=i) for i in range(20)],
        "side": ["buy"] * 20,
        "spread_at_trade": [1.0] * 20,
        "mid_price_at_trade": [21000.5] * 20,
        "best_bid_at_trade": [21000.0] * 20,
        "best_ask_at_trade": [21001.0] * 20,
        "order_type": ["market"] * 20,
    })

    # Price level history for spoofing detection (keys are tuples of (price, side))
    from collections import deque
    ob.price_level_history = {
        (21002.0, "ask"): deque([
            {"timestamp": datetime.now(UTC) - timedelta(seconds=10), "volume": 500},  # Large order appeared
            {"timestamp": datetime.now(UTC) - timedelta(seconds=5), "volume": 0},      # Then disappeared
        ], maxlen=1000),
        (20999.0, "bid"): deque([
            {"timestamp": datetime.now(UTC) - timedelta(seconds=8), "volume": 300},
            {"timestamp": datetime.now(UTC) - timedelta(seconds=3), "volume": 0},
        ], maxlen=1000),
    }

    # Mock project_x client for tick size
    mock_client = MagicMock()
    mock_client.get_instrument = AsyncMock(return_value=MagicMock(tick_size=0.25))
    ob.project_x = mock_client

    # Mock methods
    ob._get_best_bid_ask_unlocked = MagicMock(return_value={
        "bid": 21000.0,
        "ask": 21001.0,
        "spread": 1.0,
        "mid_price": 21000.5,
        "timestamp": datetime.now(UTC),
    })

    ob._get_orderbook_bids_unlocked = MagicMock(
        side_effect=lambda levels: ob.orderbook_bids.head(levels) if levels else ob.orderbook_bids
    )
    ob._get_orderbook_asks_unlocked = MagicMock(
        side_effect=lambda levels: ob.orderbook_asks.head(levels) if levels else ob.orderbook_asks
    )

    # Trade flow stats needed for detection methods
    ob.trade_flow_stats = {
        "iceberg_detected_count": 0,
        "spoofing_alerts": 0,
    }

    # Iceberg detection history
    ob.detected_icebergs = []
    ob.spoofing_alerts = []

    return ob


@pytest.fixture
def order_detection(mock_orderbook_base):
    """Create an OrderDetection instance for testing."""
    return OrderDetection(mock_orderbook_base)


class TestOrderDetectionInitialization:
    """Test OrderDetection initialization."""

    def test_initialization(self, order_detection, mock_orderbook_base):
        """Test that OrderDetection initializes correctly."""
        assert order_detection.orderbook == mock_orderbook_base
        assert hasattr(order_detection, "logger")


class TestIcebergDetection:
    """Test iceberg order detection."""

    @pytest.mark.asyncio
    async def test_detect_iceberg_orders_basic(self, order_detection):
        """Test basic iceberg order detection."""
        result = await order_detection.detect_iceberg_orders(
            min_refreshes=5,
            volume_threshold=50,
            time_window_minutes=10
        )

        assert isinstance(result, dict)
        assert "iceberg_levels" in result
        assert "analysis_window_minutes" in result
        assert "detection_parameters" in result

        # Check structure
        assert isinstance(result["iceberg_levels"], list)
        assert result["analysis_window_minutes"] == 10

    @pytest.mark.asyncio
    async def test_detect_iceberg_orders_no_pattern(self, order_detection, mock_orderbook_base):
        """Test iceberg detection with no iceberg pattern."""
        # Set up trades with varied prices (no iceberg pattern)
        mock_orderbook_base.recent_trades = pl.DataFrame({
            "price": [21000.0 + i for i in range(10)],  # Different prices
            "volume": [10 + i * 5 for i in range(10)],  # Varied volumes
            "timestamp": [datetime.now(UTC) - timedelta(seconds=i) for i in range(10)],
            "side": ["buy", "sell"] * 5,
            "spread_at_trade": [1.0] * 10,
            "mid_price_at_trade": [21000.5] * 10,
            "best_bid_at_trade": [21000.0] * 10,
            "best_ask_at_trade": [21001.0] * 10,
            "order_type": ["market"] * 10,
        })

        result = await order_detection.detect_iceberg_orders()
        assert len(result["iceberg_levels"]) == 0

    @pytest.mark.asyncio
    async def test_detect_iceberg_orders_empty_trades(self, order_detection, mock_orderbook_base):
        """Test iceberg detection with no trades."""
        mock_orderbook_base.recent_trades = pl.DataFrame({
            "price": [], "volume": [], "timestamp": [], "side": [],
            "spread_at_trade": [], "mid_price_at_trade": [],
            "best_bid_at_trade": [], "best_ask_at_trade": [],
            "order_type": [],
        })

        result = await order_detection.detect_iceberg_orders()
        assert len(result["iceberg_levels"]) == 0


class TestSpoofingDetection:
    """Test spoofing detection."""

    @pytest.mark.asyncio
    async def test_detect_spoofing_basic(self, order_detection):
        """Test basic spoofing detection."""
        result = await order_detection.detect_spoofing(
            time_window_minutes=5,
            min_placement_frequency=2.0
        )

        assert isinstance(result, list)
        # Result is list of SpoofingDetectionResponse objects
        # Could be empty if no spoofing patterns detected
        assert isinstance(result, list)

    @pytest.mark.asyncio
    async def test_detect_spoofing_no_history(self, order_detection, mock_orderbook_base):
        """Test spoofing detection with no price history."""
        mock_orderbook_base.price_level_history = {}

        result = await order_detection.detect_spoofing()
        assert isinstance(result, list)
        assert len(result) == 0

    @pytest.mark.asyncio
    async def test_detect_spoofing_stable_orders(self, order_detection, mock_orderbook_base):
        """Test spoofing detection with stable orders (no spoofing)."""
        # Set up stable order history (keys are tuples of (price, side))
        mock_orderbook_base.price_level_history = {
            (21002.0, "ask"): deque([
                {"timestamp": datetime.now(UTC) - timedelta(seconds=i), "volume": 100}
                for i in range(10)
            ], maxlen=1000),
        }

        result = await order_detection.detect_spoofing()
        assert isinstance(result, list)
        assert len(result) == 0


class TestOrderClusters:
    """Test order clustering detection."""

    @pytest.mark.asyncio
    async def test_detect_order_clusters(self, order_detection):
        """Test order clustering detection."""
        result = await order_detection.detect_order_clusters(
            min_cluster_size=3,
            price_tolerance=0.1
        )

        assert isinstance(result, list)
        # Result is list of cluster dictionaries

    @pytest.mark.asyncio
    async def test_detect_order_clusters_with_pattern(self, order_detection, mock_orderbook_base):
        """Test order clustering with clear cluster pattern."""
        # Set up clustered orders on bid side
        mock_orderbook_base.orderbook_bids = pl.DataFrame({
            "price": [21000.0, 20999.0, 20998.0, 20997.0, 20996.0],
            "volume": [100, 100, 100, 100, 100],  # Same size at multiple levels
            "timestamp": [datetime.now(UTC)] * 5,
        })

        result = await order_detection.detect_order_clusters()
        assert isinstance(result, list)
        # Could be empty or contain clusters


class TestAdvancedMarketMetrics:
    """Test advanced market metrics."""

    @pytest.mark.asyncio
    async def test_get_advanced_market_metrics(self, order_detection):
        """Test advanced market metrics calculation."""
        result = await order_detection.get_advanced_market_metrics()

        assert isinstance(result, dict)
        # Check for actual returned fields (based on error output)
        assert "bid_depth" in result
        assert "ask_depth" in result
        assert "total_bid_size" in result
        assert "total_ask_size" in result
        assert "avg_bid_size" in result
        assert "avg_ask_size" in result
        assert "price_levels" in result
        assert "order_clustering" in result
        assert "imbalance" in result
        assert "spread" in result
        assert "mid_price" in result
        assert "weighted_mid_price" in result
        assert "volume_weighted_avg_price" in result
        assert "time_weighted_avg_price" in result
        assert "timestamp" in result


class TestErrorHandling:
    """Test error handling in detection."""

    @pytest.mark.asyncio
    async def test_handle_empty_orderbook(self, order_detection, mock_orderbook_base):
        """Test all detection methods handle empty orderbook gracefully."""
        mock_orderbook_base.orderbook_bids = pl.DataFrame({
            "price": [], "volume": [], "timestamp": []
        })
        mock_orderbook_base.orderbook_asks = pl.DataFrame({
            "price": [], "volume": [], "timestamp": []
        })
        mock_orderbook_base.recent_trades = pl.DataFrame({
            "price": [], "volume": [], "timestamp": [], "side": [],
            "spread_at_trade": [], "mid_price_at_trade": [],
            "best_bid_at_trade": [], "best_ask_at_trade": [],
            "order_type": [],
        })

        # All methods should handle empty data without raising
        assert await order_detection.detect_iceberg_orders() is not None
        assert await order_detection.detect_spoofing() is not None
        assert await order_detection.detect_order_clusters() is not None
        assert await order_detection.get_advanced_market_metrics() is not None


class TestThreadSafety:
    """Test thread safety of detection operations."""

    @pytest.mark.asyncio
    async def test_concurrent_detection_operations(self, order_detection):
        """Test that concurrent detection operations are safe."""
        tasks = [
            order_detection.detect_iceberg_orders(),
            order_detection.detect_spoofing(),
            order_detection.detect_order_clusters(),
            order_detection.get_advanced_market_metrics(),
        ]

        # All should complete without deadlock
        results = await asyncio.gather(*tasks, return_exceptions=True)

        # Check that results were returned (even if detection found nothing)
        for result in results:
            assert result is not None
            assert not isinstance(result, Exception)


# Run tests with coverage reporting
if __name__ == "__main__":
    pytest.main([__file__, "-v", "--cov=src/project_x_py/orderbook/detection", "--cov-report=term-missing"])
