"""Phase 3: Feed Integration Audit - Staleness, Metrics, Dataclasses, Live Feeds.

Tests feed data staleness logic, feed metrics tracking, dataclass validation,
and (where feasible) live public feed connectivity.
"""

import time

import pytest

from horizon._horizon import Engine, FeedMetrics, FeedSnapshot, RiskConfig
from horizon.context import FeedData
from horizon.feeds import (
    BinanceWS,
    CalendarFeed,
    ChainlinkFeed,
    ESPNFeed,
    ManifoldFeed,
    NWSFeed,
    PredictItFeed,
    RESTFeed,
    RESTJsonPathFeed,
)


# ---------------------------------------------------------------------------
# 3a. Feed Staleness Tests
# ---------------------------------------------------------------------------

class TestFeedStaleness:
    """FeedData.is_stale() boundary conditions."""

    def test_zero_timestamp_is_stale(self):
        """Timestamp=0 should always be stale (never updated)."""
        fd = FeedData(timestamp=0.0)
        assert fd.is_stale() is True

    def test_negative_timestamp_is_stale(self):
        """Negative timestamp should be stale."""
        fd = FeedData(timestamp=-1.0)
        assert fd.is_stale() is True

    def test_current_timestamp_not_stale(self):
        """Timestamp near now should not be stale."""
        fd = FeedData(timestamp=time.time())
        assert fd.is_stale() is False

    def test_old_timestamp_is_stale(self):
        """Timestamp 60s ago with 30s window should be stale."""
        fd = FeedData(timestamp=time.time() - 60.0)
        assert fd.is_stale(max_age_secs=30.0) is True

    def test_custom_staleness_threshold(self):
        """Custom max_age_secs should be respected."""
        fd = FeedData(timestamp=time.time() - 10.0)
        assert fd.is_stale(max_age_secs=5.0) is True
        assert fd.is_stale(max_age_secs=15.0) is False

    def test_reference_time_used_when_set(self):
        """_reference_time should be used as 'now' when set."""
        ref = 1000.0
        fd = FeedData(timestamp=990.0, _reference_time=ref)
        assert fd.is_stale(max_age_secs=15.0) is False  # 10s < 15s
        assert fd.is_stale(max_age_secs=5.0) is True    # 10s > 5s

    def test_explicit_now_overrides_reference(self):
        """Explicit 'now' parameter should override _reference_time."""
        fd = FeedData(timestamp=990.0, _reference_time=1000.0)
        # With now=2000, delta is 1010 → stale
        assert fd.is_stale(max_age_secs=30.0, now=2000.0) is True

    def test_default_feeddata_is_stale(self):
        """Default FeedData (all zeros) should be stale."""
        fd = FeedData()
        assert fd.is_stale() is True

    def test_feeddata_fields_default(self):
        """All FeedData numeric fields default to 0."""
        fd = FeedData()
        assert fd.price == 0.0
        assert fd.bid == 0.0
        assert fd.ask == 0.0
        assert fd.volume_24h == 0.0
        assert fd.source == ""


# ---------------------------------------------------------------------------
# 3b. Feed Dataclass Validation
# ---------------------------------------------------------------------------

class TestFeedDataclasses:
    """Verify feed dataclass types and default values."""

    def test_binance_ws(self):
        f = BinanceWS(symbol="btcusdt")
        assert f._type == "binance_ws"
        assert f.symbol == "btcusdt"

    def test_rest_feed_defaults(self):
        f = RESTFeed(url="http://example.com/price")
        assert f._type == "rest"
        assert f.interval == 5.0

    def test_manifold_feed(self):
        f = ManifoldFeed(slug="will-ai-pass-turing-test")
        assert f._type == "manifold"
        assert f.interval == 5.0

    def test_espn_feed(self):
        f = ESPNFeed(sport="basketball", league="nba")
        assert f._type == "espn"
        assert f.interval == 10.0

    def test_nws_feed_forecast(self):
        f = NWSFeed(office="TOP", grid_x=31, grid_y=80)
        assert f._type == "nws"
        assert f.mode == "forecast"
        assert f.interval == 60.0

    def test_rest_json_path_feed(self):
        f = RESTJsonPathFeed(
            url="http://example.com/api",
            price_path="data.price",
        )
        assert f._type == "rest_json_path"
        assert f.price_path == "data.price"

    def test_chainlink_feed(self):
        f = ChainlinkFeed(
            contract_address="0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419",
            rpc_url="https://eth.llamarpc.com",
        )
        assert f._type == "chainlink"
        assert f.decimals == 8
        assert f.interval == 10.0

    def test_calendar_feed_loads(self):
        """CalendarFeed should load without error (bundled events)."""
        f = CalendarFeed()
        assert f._type == "calendar"

    def test_predictit_feed(self):
        f = PredictItFeed(market_id=7456)
        assert f._type == "predictit"
        assert f.contract_id is None


# ---------------------------------------------------------------------------
# 3c. Feed Engine Integration
# ---------------------------------------------------------------------------

class TestFeedEngine:
    """Test feed start/stop/metrics through the engine."""

    def test_start_and_get_rest_feed(self):
        """Start a REST feed and verify snapshot is created."""
        e = Engine(risk_config=RiskConfig())
        e.start_feed("test_feed", "rest", url="https://httpbin.org/json", interval=60.0)

        # Feed should exist (snapshot may be empty initially)
        snap = e.feed_snapshot("test_feed")
        assert snap is not None

    def test_feed_snapshot_default_values(self):
        """FeedSnapshot should have sensible defaults."""
        snap = FeedSnapshot()
        assert snap.price == 0.0
        assert snap.bid == 0.0
        assert snap.ask == 0.0
        assert snap.timestamp == 0.0

    def test_stop_feed(self):
        """Stopping a feed should not crash."""
        e = Engine(risk_config=RiskConfig())
        e.start_feed("tmp_feed", "rest", url="https://httpbin.org/json", interval=60.0)
        # Should not raise
        e.stop_feed("tmp_feed")

    def test_multiple_feeds(self):
        """Multiple feeds can coexist."""
        e = Engine(risk_config=RiskConfig())
        e.start_feed("feed_a", "rest", url="https://httpbin.org/json", interval=60.0)
        e.start_feed("feed_b", "rest", url="https://httpbin.org/json", interval=60.0)

        # Both should have snapshots
        assert e.feed_snapshot("feed_a") is not None
        assert e.feed_snapshot("feed_b") is not None

    def test_feed_metrics_exist(self):
        """After starting a feed, metrics should be accessible."""
        e = Engine(risk_config=RiskConfig())
        e.start_feed("m_feed", "rest", url="https://httpbin.org/json", interval=60.0)

        metrics = e.feed_metrics("m_feed")
        assert metrics is not None
        assert hasattr(metrics, "update_count")
        assert hasattr(metrics, "is_connected")


# ---------------------------------------------------------------------------
# 3d. Live Feed Smoke Tests (network-dependent, skip if offline)
# ---------------------------------------------------------------------------

@pytest.mark.skipif(
    not pytest.importorskip("socket", reason="network check"),
    reason="network unavailable",
)
class TestLiveFeedsSmoke:
    """Smoke tests for live public feeds. Marked slow - skip with -m 'not slow'."""

    @pytest.mark.slow
    def test_binance_ws_feed(self):
        """BinanceWS: start btcusdt, wait, verify price > 0."""
        e = Engine(risk_config=RiskConfig())
        e.start_feed("btc", "binance_ws", symbol="btcusdt")
        time.sleep(5)
        snap = e.feed_snapshot("btc")
        if snap and snap.price > 0:
            assert snap.price > 100  # BTC should be > $100
            assert snap.bid > 0
            assert snap.ask > 0
        e.stop_feed("btc")

    @pytest.mark.slow
    def test_manifold_feed(self):
        """Manifold: probability should be 0-1."""
        e = Engine(risk_config=RiskConfig())
        e.start_feed(
            "manifold_test", "manifold",
            config_json='{"slug": "will-ai-pass-turing-test-by-2029", "interval_secs": 5.0}',
        )
        time.sleep(6)
        snap = e.feed_snapshot("manifold_test")
        if snap and snap.price > 0:
            assert 0.0 <= snap.price <= 1.0
        e.stop_feed("manifold_test")
