"""Tests for DepthSignal Python SDK client."""

import json

import httpx
import pytest
from pytest_httpx import HTTPXMock

from depthsignal import (
    APIConnectionError,
    AggregatedCompositesData,
    AggregatedFeaturesData,
    AuthenticationError,
    CVDData,
    CompositeSignals,
    CorrelationData,
    DepthSignal,
    DepthSignalError,
    DepthResilienceData,
    DerivativesData,
    ExecutionCostData,
    FlowAssetData,
    FlowOverview,
    HistoricalCompositesData,
    HistoricalFeaturesData,
    LargeTradeData,
    LiquidationData,
    OrderbookFeatures,
    PositioningData,
    RateLimitError,
    SentimentData,
    SpotFuturesData,
    SpreadEventsData,
    SupportResistanceZones,
    SymbolInfo,
    TierAccessError,
    VenueSelectionData,
    WhaleFlowData,
)

BASE_URL = "https://api.depthsignal.io"
API_KEY = "test-key-12345"


# ---------------------------------------------------------------------------
# Constructor validation
# ---------------------------------------------------------------------------


class TestConstructor:
    def test_valid_key(self):
        ds = DepthSignal(api_key=API_KEY)
        assert ds._api_key == API_KEY
        ds.close()

    def test_empty_key_raises(self):
        with pytest.raises(ValueError, match="api_key is required"):
            DepthSignal(api_key="")

    def test_whitespace_key_raises(self):
        with pytest.raises(ValueError, match="api_key is required"):
            DepthSignal(api_key="   ")

    def test_key_stripped(self):
        ds = DepthSignal(api_key="  key123  ")
        assert ds._api_key == "key123"
        ds.close()

    def test_https_enforced(self):
        with pytest.raises(ValueError, match="HTTPS"):
            DepthSignal(api_key=API_KEY, base_url="http://evil.com")

    def test_localhost_http_allowed(self):
        ds = DepthSignal(api_key=API_KEY, base_url="http://localhost:8020")
        ds.close()

    def test_127_http_allowed(self):
        ds = DepthSignal(api_key=API_KEY, base_url="http://127.0.0.1:8020")
        ds.close()

    def test_trailing_slash_stripped(self):
        ds = DepthSignal(api_key=API_KEY, base_url="https://api.depthsignal.io/")
        assert ds._base_url == "https://api.depthsignal.io"
        ds.close()

    def test_context_manager(self):
        with DepthSignal(api_key=API_KEY) as ds:
            assert ds._api_key == API_KEY


# ---------------------------------------------------------------------------
# Error mapping
# ---------------------------------------------------------------------------


class TestErrorMapping:
    def _ds(self):
        """Client with retries disabled so error tests don't sleep."""
        return DepthSignal(api_key=API_KEY, max_retries=0)

    def test_401_raises_authentication_error(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(
            status_code=401,
            json={"detail": "Invalid API key"},
        )
        ds = self._ds()
        with pytest.raises(AuthenticationError) as exc:
            ds.health()
        assert exc.value.status_code == 401
        ds.close()

    def test_403_raises_tier_access_error(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(
            status_code=403,
            json={"detail": "Pro tier required"},
        )
        ds = self._ds()
        with pytest.raises(TierAccessError) as exc:
            ds.composites("BTCUSDT")
        assert exc.value.status_code == 403
        ds.close()

    def test_429_raises_rate_limit_error(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(
            status_code=429,
            headers={"Retry-After": "30"},
            json={"detail": "Rate limit exceeded"},
        )
        ds = self._ds()
        with pytest.raises(RateLimitError) as exc:
            ds.health()
        assert exc.value.retry_after == 30
        ds.close()

    def test_429_no_retry_header(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(status_code=429, json={"detail": "slow down"})
        ds = self._ds()
        with pytest.raises(RateLimitError) as exc:
            ds.health()
        assert exc.value.retry_after is None
        ds.close()

    def test_500_raises_depth_signal_error(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(status_code=500, json={"detail": "Server error"})
        ds = self._ds()
        with pytest.raises(DepthSignalError) as exc:
            ds.health()
        assert exc.value.status_code == 500
        ds.close()

    def test_non_json_200_raises(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(status_code=200, text="<html>oops</html>")
        ds = self._ds()
        with pytest.raises(DepthSignalError, match="non-JSON"):
            ds.health()
        ds.close()

    def test_connection_error(self, httpx_mock: HTTPXMock):
        httpx_mock.add_exception(httpx.ConnectError("refused"))
        ds = self._ds()
        with pytest.raises(APIConnectionError):
            ds.health()
        ds.close()

    def test_timeout_error(self, httpx_mock: HTTPXMock):
        httpx_mock.add_exception(httpx.ReadTimeout("timeout"))
        ds = self._ds()
        with pytest.raises(APIConnectionError, match="timed out"):
            ds.health()
        ds.close()


# ---------------------------------------------------------------------------
# Retry behavior
# ---------------------------------------------------------------------------


class TestRetry:
    def test_429_retries_then_succeeds(self, httpx_mock: HTTPXMock):
        """429 with Retry-After should retry and succeed on next attempt."""
        httpx_mock.add_response(
            status_code=429,
            headers={"Retry-After": "0"},
            json={"detail": "Rate limited"},
        )
        httpx_mock.add_response(json={"status": "healthy"})
        ds = DepthSignal(api_key=API_KEY, max_retries=1)
        result = ds.health()
        assert result["status"] == "healthy"
        ds.close()

    def test_503_retries_then_succeeds(self, httpx_mock: HTTPXMock):
        """503 should retry and succeed on next attempt."""
        httpx_mock.add_response(status_code=503, json={"detail": "unavailable"})
        httpx_mock.add_response(json={"status": "healthy"})
        ds = DepthSignal(api_key=API_KEY, max_retries=1)
        result = ds.health()
        assert result["status"] == "healthy"
        ds.close()

    def test_connect_error_retries_then_succeeds(self, httpx_mock: HTTPXMock):
        """Network error should retry and succeed on next attempt."""
        httpx_mock.add_exception(httpx.ConnectError("refused"))
        httpx_mock.add_response(json={"status": "healthy"})
        ds = DepthSignal(api_key=API_KEY, max_retries=1)
        result = ds.health()
        assert result["status"] == "healthy"
        ds.close()

    def test_exhausted_retries_raises(self, httpx_mock: HTTPXMock):
        """Exhausted retries should raise the error."""
        httpx_mock.add_exception(httpx.ConnectError("refused"))
        httpx_mock.add_exception(httpx.ConnectError("refused"))
        ds = DepthSignal(api_key=API_KEY, max_retries=1)
        with pytest.raises(APIConnectionError):
            ds.health()
        ds.close()


# ---------------------------------------------------------------------------
# Endpoint methods
# ---------------------------------------------------------------------------


class TestEndpoints:
    def _ds(self):
        return DepthSignal(api_key=API_KEY)

    def test_orderbook_features(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(json={
            "symbol": "BTCUSDT",
            "features": {"binance": {"vpin": 0.5}},
            "features_count": 1,
        })
        ds = self._ds()
        result = ds.orderbook_features("BTCUSDT")
        assert isinstance(result, OrderbookFeatures)
        assert result.symbol == "BTCUSDT"
        assert result.features["binance"]["vpin"] == 0.5
        ds.close()

    def test_composites(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(json={
            "symbol": "BTCUSDT",
            "composites": {"directional_pressure": 0.7},
        })
        ds = self._ds()
        result = ds.composites("BTCUSDT")
        assert isinstance(result, CompositeSignals)
        assert result.composites["directional_pressure"] == 0.7
        ds.close()

    def test_support_resistance(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(json={
            "symbol": "BTCUSDT",
            "zones": [{"price": 50000, "strength": 0.9, "type": "support"}],
        })
        ds = self._ds()
        result = ds.support_resistance("BTCUSDT")
        assert isinstance(result, SupportResistanceZones)
        assert len(result.zones) == 1
        ds.close()

    def test_flow_overview(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(json={
            "exchanges": {"binance": {"status": "connected"}},
        })
        ds = self._ds()
        result = ds.flow_overview()
        assert isinstance(result, FlowOverview)
        ds.close()

    def test_flow_asset(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(json={"symbol": "BTCUSDT", "binance": {"depth": 100}})
        ds = self._ds()
        result = ds.flow_asset("BTCUSDT")
        assert isinstance(result, FlowAssetData)
        assert result.symbol == "BTCUSDT"
        ds.close()

    def test_liquidations(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(json={
            "symbol": "BTCUSDT",
            "rate_per_minute": 5.0,
            "cascade_risk": 0.3,
        })
        ds = self._ds()
        result = ds.liquidations("BTCUSDT")
        assert isinstance(result, LiquidationData)
        assert result.cascade_risk == 0.3
        ds.close()

    def test_large_trades(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(json={
            "symbol": "BTCUSDT",
            "count_5m": 12,
            "buy_sell_ratio": 1.5,
            "institutional_score": 0.8,
        })
        ds = self._ds()
        result = ds.large_trades("BTCUSDT")
        assert isinstance(result, LargeTradeData)
        assert result.count_5m == 12
        ds.close()

    def test_cvd(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(json={
            "symbol": "BTCUSDT",
            "cvd_1m": 100.0,
            "cvd_5m": 500.0,
            "cvd_slope": 0.1,
            "taker_ratio": 0.6,
        })
        ds = self._ds()
        result = ds.cvd("BTCUSDT")
        assert isinstance(result, CVDData)
        assert result.cvd_1m == 100.0
        ds.close()

    def test_spot_futures(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(json={
            "symbol": "BTCUSDT",
            "basis_spread": 0.001,
            "depth_divergence": 0.5,
            "ofi_lead_lag": 0.2,
            "whale_agreement": 0.9,
            "liquidity_ratio": 1.1,
        })
        ds = self._ds()
        result = ds.spot_futures("BTCUSDT")
        assert isinstance(result, SpotFuturesData)
        assert result.basis_spread == 0.001
        ds.close()

    def test_derivatives(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(json={
            "symbol": "BTCUSDT",
            "funding_agreement": 0.7,
            "oi_divergence": 0.3,
            "ls_consensus": 0.6,
        })
        ds = self._ds()
        result = ds.derivatives("BTCUSDT")
        assert isinstance(result, DerivativesData)
        assert result.funding_agreement == 0.7
        ds.close()

    def test_positioning(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(json={
            "symbol": "BTCUSDT",
            "positioning": {"net_long": 0.6},
        })
        ds = self._ds()
        result = ds.positioning("BTCUSDT")
        assert isinstance(result, PositioningData)
        ds.close()

    def test_symbols(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(json=[
            {"symbol": "BTCUSDT", "baseAsset": "BTC", "quoteAsset": "USDT", "exchange": "all"},
        ])
        ds = self._ds()
        result = ds.symbols()
        assert len(result) == 1
        assert isinstance(result[0], SymbolInfo)
        assert result[0].symbol == "BTCUSDT"
        assert result[0].base_asset == "BTC"
        ds.close()

    def test_tiers(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(json={"free": {"price": 0}})
        ds = self._ds()
        result = ds.tiers()
        assert "free" in result
        ds.close()

    def test_health(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(json={"status": "healthy"})
        ds = self._ds()
        result = ds.health()
        assert result["status"] == "healthy"
        ds.close()

    def test_historical_features(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(json={"symbol": "BTCUSDT", "data": []})
        ds = self._ds()
        result = ds.historical_features("BTCUSDT", exchange="binance", hours=24, limit=100)
        assert isinstance(result, HistoricalFeaturesData)
        assert result.symbol == "BTCUSDT"
        ds.close()

    def test_historical_composites(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(json={"symbol": "BTCUSDT", "data": []})
        ds = self._ds()
        result = ds.historical_composites("BTCUSDT", hours=12, limit=50)
        assert isinstance(result, HistoricalCompositesData)
        assert result.symbol == "BTCUSDT"
        ds.close()

    def test_symbol_url_encoded(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(json={
            "symbol": "BTC/USDT",
            "features": {},
            "features_count": 0,
        })
        ds = self._ds()
        ds.orderbook_features("BTC/USDT")
        request = httpx_mock.get_request()
        assert "BTC%2FUSDT" in str(request.url)
        ds.close()


# ---------------------------------------------------------------------------
# v0.2.0 endpoint methods
# ---------------------------------------------------------------------------


class TestV020Endpoints:
    def _ds(self):
        return DepthSignal(api_key=API_KEY)

    def test_sentiment(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(json={
            "symbol": "BTCUSDT",
            "sentiment": {"fear_greed": 65, "momentum": 0.3},
        })
        ds = self._ds()
        result = ds.sentiment("BTCUSDT")
        assert isinstance(result, SentimentData)
        assert result.symbol == "BTCUSDT"
        assert result.sentiment["fear_greed"] == 65
        ds.close()

    def test_aggregated_features(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(json={
            "symbol": "BTCUSDT",
            "exchange": "binance",
            "timeframe": "5m",
            "features": {"vpin": 0.45},
            "source": "precomputed",
        })
        ds = self._ds()
        result = ds.aggregated_features("BTCUSDT")
        assert isinstance(result, AggregatedFeaturesData)
        assert result.symbol == "BTCUSDT"
        assert result.timeframe == "5m"
        assert result.features["vpin"] == 0.45
        ds.close()

    def test_aggregated_features_with_timeframe(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(json={
            "symbol": "BTCUSDT",
            "exchange": "",
            "timeframe": "1h",
            "features": {},
            "source": "on-demand",
        })
        ds = self._ds()
        ds.aggregated_features("BTCUSDT", timeframe="1h")
        request = httpx_mock.get_request()
        assert "timeframe=1h" in str(request.url)
        ds.close()

    def test_aggregated_composites(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(json={
            "symbol": "BTCUSDT",
            "timeframe": "4h",
            "composites": {"directional_pressure": 0.6},
            "source": "precomputed",
        })
        ds = self._ds()
        result = ds.aggregated_composites("BTCUSDT")
        assert isinstance(result, AggregatedCompositesData)
        assert result.composites["directional_pressure"] == 0.6
        ds.close()

    def test_venue_selection(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(json={
            "symbol": "BTCUSDT",
            "recommended_venue": "binance",
            "best_spread_bps": 0.5,
            "worst_spread_bps": 3.2,
            "exchanges_checked": 4,
            "venues": [{"exchange": "binance", "spread_bps": 0.5}],
        })
        ds = self._ds()
        result = ds.venue_selection("BTCUSDT")
        assert isinstance(result, VenueSelectionData)
        assert result.recommended_venue == "binance"
        assert result.exchanges_checked == 4
        ds.close()

    def test_depth_resilience(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(json={
            "symbol": "BTCUSDT",
            "deepest_venue": "binance",
            "exchanges_checked": 3,
            "venues": [{"exchange": "binance", "depth_10bp": 500000}],
        })
        ds = self._ds()
        result = ds.depth_resilience("BTCUSDT")
        assert isinstance(result, DepthResilienceData)
        assert result.deepest_venue == "binance"
        assert result.exchanges_checked == 3
        ds.close()

    def test_spread_events(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(json={
            "symbol": "BTCUSDT",
            "threshold": 3.0,
            "median_spread_bps": 1.2,
            "active_events": 1,
            "status": "widening",
            "events": [{"exchange": "okx", "spread_bps": 5.1}],
            "exchanges_checked": 5,
        })
        ds = self._ds()
        result = ds.spread_events("BTCUSDT")
        assert isinstance(result, SpreadEventsData)
        assert result.active_events == 1
        assert result.status == "widening"
        ds.close()

    def test_spread_events_with_threshold(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(json={
            "symbol": "BTCUSDT",
            "threshold": 5.0,
            "median_spread_bps": 1.0,
            "active_events": 0,
            "status": "normal",
            "events": [],
            "exchanges_checked": 5,
        })
        ds = self._ds()
        ds.spread_events("BTCUSDT", threshold=5.0)
        request = httpx_mock.get_request()
        assert "threshold=5.0" in str(request.url)
        ds.close()

    def test_execution_cost(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(json={
            "symbol": "BTCUSDT",
            "trade_size_usd": 10000.0,
            "cheapest_venue": "binance",
            "cheapest_cost_usd": 1.5,
            "most_expensive_venue": "okx",
            "most_expensive_cost_usd": 4.2,
            "potential_savings_usd": 2.7,
            "exchanges_compared": 3,
            "venues": [],
        })
        ds = self._ds()
        result = ds.execution_cost("BTCUSDT")
        assert isinstance(result, ExecutionCostData)
        assert result.cheapest_venue == "binance"
        assert result.potential_savings_usd == 2.7
        ds.close()

    def test_execution_cost_with_trade_size(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(json={
            "symbol": "BTCUSDT",
            "trade_size_usd": 50000.0,
            "cheapest_venue": "binance",
            "exchanges_compared": 3,
            "venues": [],
        })
        ds = self._ds()
        ds.execution_cost("BTCUSDT", trade_size_usd=50000.0)
        request = httpx_mock.get_request()
        assert "trade_size_usd=50000.0" in str(request.url)
        ds.close()

    def test_correlation(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(json={
            "symbol1": "BTCUSDT",
            "symbol2": "ETHUSDT",
            "correlation": 0.87,
            "interpretation": "strong positive",
            "common_exchanges": 4,
            "exchanges": ["binance", "okx", "bybit", "coinbase"],
            "status": "ok",
        })
        ds = self._ds()
        result = ds.correlation("BTCUSDT", "ETHUSDT")
        assert isinstance(result, CorrelationData)
        assert result.correlation == 0.87
        assert result.interpretation == "strong positive"
        request = httpx_mock.get_request()
        assert "BTCUSDT" in str(request.url)
        assert "ETHUSDT" in str(request.url)
        ds.close()

    def test_whale_flow(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(json={
            "symbol": "BTCUSDT",
            "aggregate_direction": 0.35,
            "aggregate_pressure": "bullish",
            "avg_whale_bid_fraction": 0.12,
            "avg_whale_ask_fraction": 0.08,
            "exchanges_reporting": 5,
            "venues": [{"exchange": "binance", "whale_bid_fraction": 0.15}],
        })
        ds = self._ds()
        result = ds.whale_flow("BTCUSDT")
        assert isinstance(result, WhaleFlowData)
        assert result.aggregate_direction == 0.35
        assert result.aggregate_pressure == "bullish"
        assert result.exchanges_reporting == 5
        ds.close()

    def test_historical_features_typed(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(json={
            "symbol": "BTCUSDT",
            "exchange": "binance",
            "data": [{"ts": "2026-01-01", "vpin": 0.5}],
        })
        ds = self._ds()
        result = ds.historical_features("BTCUSDT", exchange="binance", hours=24)
        assert isinstance(result, HistoricalFeaturesData)
        assert result.symbol == "BTCUSDT"
        assert result.count == 1
        ds.close()

    def test_historical_composites_typed(self, httpx_mock: HTTPXMock):
        httpx_mock.add_response(json={
            "symbol": "BTCUSDT",
            "data": [{"ts": "2026-01-01", "dp": 0.7}, {"ts": "2026-01-02", "dp": 0.8}],
        })
        ds = self._ds()
        result = ds.historical_composites("BTCUSDT", hours=48)
        assert isinstance(result, HistoricalCompositesData)
        assert result.symbol == "BTCUSDT"
        assert result.count == 2
        ds.close()


# ---------------------------------------------------------------------------
# Model from_dict() edge cases
# ---------------------------------------------------------------------------


class TestFromDict:
    def test_orderbook_features_empty_dict(self):
        result = OrderbookFeatures.from_dict({})
        assert result.symbol == ""
        assert result.features == {}
        assert result.features_count == 0
        assert result.data_age_seconds is None

    def test_composite_signals_empty_dict(self):
        result = CompositeSignals.from_dict({})
        assert result.symbol == ""
        assert result.composites == {}

    def test_liquidation_data_partial_dict(self):
        result = LiquidationData.from_dict({"symbol": "BTCUSDT"})
        assert result.symbol == "BTCUSDT"
        assert result.rate_per_minute == 0.0
        assert result.cascade_risk == 0.0

    def test_cvd_data_extra_keys_ignored(self):
        result = CVDData.from_dict({
            "symbol": "BTCUSDT",
            "cvd_1m": 100.0,
            "unknown_field": "ignored",
        })
        assert result.cvd_1m == 100.0
        assert not hasattr(result, "unknown_field")

    def test_flow_asset_separates_exchanges(self):
        result = FlowAssetData.from_dict({
            "symbol": "BTCUSDT",
            "timestamp": "2026-01-01",
            "binance": {"depth": 100},
            "okx": {"depth": 50},
        })
        assert result.exchanges["binance"]["depth"] == 100
        assert result.exchanges["okx"]["depth"] == 50
        assert "symbol" not in result.exchanges

    def test_symbol_info_defaults(self):
        result = SymbolInfo.from_dict({"symbol": "XYZUSDT"})
        assert result.symbol == "XYZUSDT"
        assert result.base_asset == ""
        assert result.quote_asset == ""
        assert result.exchange == "all"


# ---------------------------------------------------------------------------
# v0.2.0 model from_dict() edge cases
# ---------------------------------------------------------------------------


class TestV020FromDict:
    def test_sentiment_data_fallback(self):
        """When 'sentiment' key is missing, non-reserved keys are collected."""
        result = SentimentData.from_dict({
            "symbol": "BTCUSDT",
            "timestamp": "2026-01-01",
            "fear_greed": 72,
            "momentum": 0.4,
        })
        assert result.symbol == "BTCUSDT"
        assert result.sentiment["fear_greed"] == 72
        assert result.sentiment["momentum"] == 0.4
        assert "symbol" not in result.sentiment
        assert "timestamp" not in result.sentiment

    def test_venue_selection_defaults(self):
        result = VenueSelectionData.from_dict({})
        assert result.symbol == ""
        assert result.recommended_venue is None
        assert result.best_spread_bps is None
        assert result.worst_spread_bps is None
        assert result.cost_ratio is None
        assert result.exchanges_checked == 0
        assert result.venues == []

    def test_correlation_data_defaults(self):
        result = CorrelationData.from_dict({})
        assert result.symbol1 == ""
        assert result.symbol2 == ""
        assert result.correlation is None
        assert result.interpretation == "unknown"
        assert result.common_exchanges == 0
        assert result.exchanges == []
        assert result.status == "ok"

    def test_whale_flow_data_defaults(self):
        result = WhaleFlowData.from_dict({})
        assert result.symbol == ""
        assert result.aggregate_direction == 0.0
        assert result.aggregate_pressure == "neutral"
        assert result.avg_whale_bid_fraction == 0.0
        assert result.avg_whale_ask_fraction == 0.0
        assert result.exchanges_reporting == 0
        assert result.venues == []

    def test_historical_features_data_count(self):
        result = HistoricalFeaturesData.from_dict({
            "symbol": "ETHUSDT",
            "data": [{"a": 1}, {"a": 2}, {"a": 3}],
        })
        assert result.count == 3
        assert result.count == len(result.data)
