"""Audit gap coverage: Live feeds, Dashboard imports, Calibration tracker,
Multi-exchange routing, Historical fetchers, and Python analytics.

Covers the remaining untested modules: dashboard smoke test, calibration
persistence, multi-exchange engine, and Python-side analytics computation.
"""

import os
import tempfile
import time

import pytest

from horizon._horizon import (
    Engine,
    Fill,
    Market,
    OrderRequest,
    OrderSide,
    OrderType,
    Quote,
    RiskConfig,
    Side,
)
from horizon.context import Context, FeedData, InventorySnapshot


RELAXED_RISK = RiskConfig(
    max_position_per_market=10000.0,
    max_portfolio_notional=100000.0,
    max_daily_drawdown_pct=100.0,
    max_order_size=10000.0,
    rate_limit_sustained=1_000_000,
    rate_limit_burst=1_000_000,
    dedup_window_ms=0,
)


# ===================================================================
# Dashboard Smoke Test
# ===================================================================

class TestDashboardImports:
    """Verify dashboard module is importable and panel classes exist."""

    def test_import_tui_module(self):
        from horizon.dashboard import tui
        assert hasattr(tui, "HorizonApp")

    def test_import_panel_classes(self):
        from horizon.dashboard.tui import (
            PnLPanel,
            PositionsPanel,
            OrdersPanel,
            RiskPanel,
            FillsPanel,
            FeedsPanel,
        )
        # Just verify imports - no instantiation (requires Textual runtime)
        assert PnLPanel is not None
        assert FeedsPanel is not None

    def test_sparkline_helper(self):
        """_sparkline should produce ASCII sparkline."""
        from horizon.dashboard.tui import _sparkline
        result = _sparkline([1.0, 2.0, 3.0, 2.0, 1.0])
        assert isinstance(result, str)
        assert len(result) > 0


# ===================================================================
# Calibration Tracker
# ===================================================================

class TestCalibrationTracker:
    """CalibrationTracker: log predictions, resolve markets, compute metrics."""

    def test_calibration_tracker_basic(self):
        from horizon.calibration import CalibrationTracker
        ct = CalibrationTracker(db_path=":memory:")

        # Log predictions
        ct.log_prediction("mkt_1", 0.70)
        ct.log_prediction("mkt_2", 0.30)

        # Resolve markets
        ct.resolve_market("mkt_1", True)
        ct.resolve_market("mkt_2", False)

        # Brier score should be computable
        brier = ct.brier_score()
        assert isinstance(brier, float)
        # (0.70 - 1.0)^2 + (0.30 - 0.0)^2 = 0.09 + 0.09 = 0.18, avg = 0.09
        assert brier == pytest.approx(0.09, abs=0.01)

    def test_calibration_tracker_log_loss(self):
        from horizon.calibration import CalibrationTracker
        ct = CalibrationTracker(db_path=":memory:")
        ct.log_prediction("mkt_1", 0.90)
        ct.resolve_market("mkt_1", True)

        ll = ct.log_loss()
        assert isinstance(ll, float)
        assert ll > 0

    def test_calibration_curve(self):
        from horizon.calibration import CalibrationTracker
        ct = CalibrationTracker(db_path=":memory:")

        # Log many predictions across probability range
        for i in range(100):
            p = (i + 1) / 101.0
            market_id = f"mkt_{i}"
            ct.log_prediction(market_id, p)
            # Resolve: markets with prob > 0.5 tend to resolve True
            ct.resolve_market(market_id, p > 0.5)

        curve = ct.calibration_curve(n_bins=5)
        assert isinstance(curve, list)
        assert len(curve) == 5

    def test_calibration_tracker_db_persistence(self):
        """Predictions should persist in SQLite."""
        from horizon.calibration import CalibrationTracker

        with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
            db_path = f.name

        try:
            ct1 = CalibrationTracker(db_path=db_path)
            ct1.log_prediction("mkt_1", 0.60)
            ct1.resolve_market("mkt_1", True)
            brier1 = ct1.brier_score()

            # Reopen from same DB
            ct2 = CalibrationTracker(db_path=db_path)
            brier2 = ct2.brier_score()
            assert brier1 == pytest.approx(brier2)
        finally:
            os.unlink(db_path)

    def test_calibration_overconfidence(self):
        from horizon.calibration import CalibrationTracker, CalibrationReport
        ct = CalibrationTracker(db_path=":memory:")

        # Overconfident predictions: say 90% but only 50% happen
        for i in range(20):
            ct.log_prediction(f"mkt_{i}", 0.90)
            ct.resolve_market(f"mkt_{i}", i < 10)  # 50% resolve True

        brier = ct.brier_score()
        # Overconfident → higher Brier score
        assert brier > 0.1


# ===================================================================
# Python Analytics (compute_metrics)
# ===================================================================

class TestPythonAnalytics:
    """Python-side analytics computation."""

    def test_compute_metrics(self):
        from horizon.analytics import compute_metrics

        equity_curve = [(float(i), 100.0 + i * 0.5) for i in range(50)]

        # Use actual Fill objects from Rust
        fills = [
            Fill(fill_id="f1", order_id="o1", market_id="mkt_1",
                 side=Side.Yes, order_side=OrderSide.Buy,
                 price=0.50, size=10.0),
            Fill(fill_id="f2", order_id="o2", market_id="mkt_1",
                 side=Side.Yes, order_side=OrderSide.Sell,
                 price=0.55, size=10.0),
        ]

        metrics = compute_metrics(equity_curve, fills, 100.0)
        assert hasattr(metrics, "total_return")
        assert hasattr(metrics, "sharpe_ratio")
        assert hasattr(metrics, "max_drawdown")

    def test_compute_trade_pnls(self):
        from horizon.analytics import _compute_trade_pnls

        class MockFill:
            def __init__(self, market_id, order_side, price, size, fee=0.0):
                self.market_id = market_id
                self.order_side = order_side
                self.price = price
                self.size = size
                self.fee = fee

        trades = [
            MockFill("mkt_1", "buy", 0.50, 10.0),
            MockFill("mkt_1", "sell", 0.55, 10.0),
        ]
        pnls = _compute_trade_pnls(trades)
        assert isinstance(pnls, list)
        # Buy at 0.50, sell at 0.55: PnL = 0.05 * 10 = 0.5
        assert len(pnls) > 0
        assert sum(pnls) > 0

    def test_compute_brier_score(self):
        from horizon.analytics import _compute_brier_score

        class MockFill:
            def __init__(self, market_id, order_side, price, size, fee=0.0):
                self.market_id = market_id
                self.order_side = order_side
                self.price = price
                self.size = size
                self.fee = fee

        trades = [
            MockFill("mkt_1", "buy", 0.70, 10.0),
            MockFill("mkt_2", "buy", 0.30, 10.0),
        ]
        outcomes = {"mkt_1": 1.0, "mkt_2": 0.0}
        brier = _compute_brier_score(trades, outcomes)
        assert isinstance(brier, float)
        # Prediction 0.70 for outcome 1.0: (0.70-1.0)^2 = 0.09
        # Prediction 0.30 for outcome 0.0: (0.30-0.0)^2 = 0.09
        assert brier == pytest.approx(0.09, abs=0.02)


# ===================================================================
# Exchange Info & Routing
# ===================================================================

class TestExchangeInfo:
    """Engine exchange information and routing methods."""

    def test_paper_exchange_name(self):
        """Default engine has paper exchange."""
        e = Engine(risk_config=RELAXED_RISK)
        assert e.exchange_name() == "paper"

    def test_paper_exchange_names(self):
        """exchange_names() returns list with paper."""
        e = Engine(risk_config=RELAXED_RISK)
        names = e.exchange_names()
        assert isinstance(names, list)
        assert "paper" in names

    def test_book_sim_exchange_name(self):
        """BookSim engine has book_sim exchange."""
        e = Engine(risk_config=RELAXED_RISK, exchange_type="book_sim",
                   fill_model="deterministic")
        assert e.exchange_name() == "book_sim"

    def test_exchange_count(self):
        """exchange_count() returns at least 1."""
        e = Engine(risk_config=RELAXED_RISK)
        assert e.exchange_count() >= 1


# ===================================================================
# Live Feed Smoke Tests (network-dependent)
# ===================================================================

class TestLiveFeeds:
    """Live feed connection tests. Skipped if network unavailable."""

    @pytest.mark.slow
    def test_binance_ws_btcusdt(self):
        """BinanceWS: connect to btcusdt, verify price update."""
        e = Engine(risk_config=RiskConfig())
        e.start_feed("btc", "binance_ws", symbol="btcusdt")

        time.sleep(5)
        snap = e.feed_snapshot("btc")
        e.stop_feed("btc")

        if snap and snap.price > 0:
            assert snap.price > 100
            assert snap.bid > 0 or snap.ask > 0

    @pytest.mark.slow
    def test_manifold_feed(self):
        """Manifold REST feed: verify probability in [0, 1]."""
        e = Engine(risk_config=RiskConfig())
        e.start_feed(
            "manifold", "manifold",
            config_json='{"slug": "will-ai-pass-turing-test-by-2029", "interval_secs": 5.0}',
        )

        time.sleep(6)
        snap = e.feed_snapshot("manifold")
        e.stop_feed("manifold")

        if snap and snap.price > 0:
            assert 0.0 <= snap.price <= 1.0

    @pytest.mark.slow
    def test_espn_feed(self):
        """ESPN feed: verify no crash."""
        e = Engine(risk_config=RiskConfig())
        e.start_feed(
            "espn", "espn",
            config_json='{"sport": "basketball", "league": "nba", "interval_secs": 10.0}',
        )

        time.sleep(12)
        e.stop_feed("espn")

    @pytest.mark.slow
    def test_nws_feed(self):
        """NWS weather feed: verify no crash."""
        e = Engine(risk_config=RiskConfig())
        e.start_feed(
            "weather", "nws",
            config_json='{"mode": "forecast", "office": "TOP", "grid_x": 31, "grid_y": 80, "interval_secs": 60.0}',
        )

        time.sleep(5)
        e.stop_feed("weather")

    @pytest.mark.slow
    def test_calendar_feed(self):
        """Calendar feed with bundled events."""
        e = Engine(risk_config=RiskConfig())
        try:
            from horizon.feeds import CalendarFeed
            cf = CalendarFeed()
            if cf.events_json and cf.events_json != "[]":
                e.start_feed(
                    "calendar", "calendar",
                    config_json=f'{{"events_json": {cf.events_json}, "interval_secs": 10.0}}',
                )
                time.sleep(3)
                e.stop_feed("calendar")
        except ImportError:
            pytest.skip("CalendarFeed not available")


# ===================================================================
# Streaming Detectors
# ===================================================================

class TestStreamingDetectors:
    """Streaming analytics: VPIN, CUSUM, OFI detectors."""

    def test_vpin_detector(self):
        """VpinDetector construction and basic usage."""
        try:
            from horizon._horizon import VpinDetector
            det = VpinDetector(bucket_volume=100.0, n_buckets=50)
            det.update(0.50, 10.0, True)  # price, size, is_buy
            det.update(0.51, 5.0, False)
            vpin = det.current_vpin()
            assert isinstance(vpin, float)
        except ImportError:
            pytest.skip("VpinDetector not available")

    def test_cusum_detector(self):
        """CusumDetector construction and basic usage."""
        try:
            from horizon._horizon import CusumDetector
            det = CusumDetector(0.01)
            for i in range(20):
                signal = det.update(0.50 + i * 0.002)
                assert isinstance(signal, bool)
        except ImportError:
            pytest.skip("CusumDetector not available")

    def test_ofi_tracker(self):
        """OfiTracker for order flow imbalance."""
        try:
            from horizon._horizon import OfiTracker
            tracker = OfiTracker()
            ofi = tracker.update(100.0, 80.0)  # best_bid_qty, best_ask_qty
            assert isinstance(ofi, float)
        except ImportError:
            pytest.skip("OfiTracker not available")


# ===================================================================
# Backtest Result Methods
# ===================================================================

class TestBacktestResult:
    """BacktestResult methods: calibration, pnl_by_market, to_csv."""

    def test_pnl_by_market(self):
        from horizon.backtest import backtest

        def quoter(ctx: Context) -> list:
            p = ctx.feed.price
            if p > 0:
                return [Quote(bid=p - 0.03, ask=p + 0.03, size=5.0)]
            return []

        data = [{"timestamp": float(i), "price": 0.50 + (i % 5) * 0.02}
                for i in range(30)]

        result = backtest(
            name="pnl_test",
            markets=["mkt_1"],
            data=data,
            pipeline=[quoter],
            initial_capital=100.0,
        )

        pnl = result.pnl_by_market()
        assert isinstance(pnl, dict)

    def test_to_csv_equity(self):
        from horizon.backtest import backtest

        data = [{"timestamp": float(i), "price": 0.50} for i in range(10)]
        result = backtest(
            name="csv_test",
            markets=["mkt_1"],
            data=data,
            pipeline=[lambda ctx: []],
            initial_capital=100.0,
        )

        with tempfile.NamedTemporaryFile(suffix=".csv", delete=False) as f:
            csv_path = f.name

        try:
            result.to_csv(csv_path, what="equity")
            assert os.path.exists(csv_path)
            with open(csv_path) as f:
                content = f.read()
                assert len(content) > 0
        finally:
            os.unlink(csv_path)


# ===================================================================
# Alpha Decay & Feature Importance
# ===================================================================

class TestAlphaDecay:
    """Alpha decay tracking and signal quality metrics."""

    def test_alpha_decay_tracker(self):
        from horizon.alpha_decay import AlphaDecayTracker
        tracker = AlphaDecayTracker(window=20)

        for i in range(30):
            signal = [0.01 * (20 - i)]  # Decaying signal
            outcome = [1.0 if i < 15 else 0.0]
            tracker.update(signal, outcome, timestamp=float(i))

        report = tracker.report()
        assert hasattr(report, "current_ic")
        assert hasattr(report, "half_life")

    def test_alpha_decay_pipeline(self):
        """Pipeline function version of alpha decay."""
        from horizon.alpha_decay import alpha_decay_pipeline
        fn = alpha_decay_pipeline(window=10)
        assert callable(fn)


# ===================================================================
# Inventory Snapshot
# ===================================================================

class TestInventorySnapshot:
    """InventorySnapshot methods: net, net_for_market, net_for_event."""

    def test_empty_inventory(self):
        inv = InventorySnapshot(positions=[])
        assert inv.net == 0.0

    def test_inventory_net_for_market(self):
        """net_for_market returns per-market net position."""
        e = Engine(risk_config=RELAXED_RISK)
        e.process_fill(Fill(
            fill_id="f1", order_id="o1", market_id="mkt_1",
            side=Side.Yes, order_side=OrderSide.Buy,
            price=0.50, size=10.0,
        ))

        inv = InventorySnapshot(positions=e.positions())
        net = inv.net_for_market("mkt_1")
        assert net == pytest.approx(10.0)

    def test_inventory_net_for_event(self):
        """net_for_event sums across multiple markets."""
        e = Engine(risk_config=RELAXED_RISK)
        e.process_fill(Fill(
            fill_id="f1", order_id="o1", market_id="mkt_A",
            side=Side.Yes, order_side=OrderSide.Buy,
            price=0.50, size=10.0,
        ))
        e.process_fill(Fill(
            fill_id="f2", order_id="o2", market_id="mkt_B",
            side=Side.Yes, order_side=OrderSide.Buy,
            price=0.50, size=5.0,
        ))

        inv = InventorySnapshot(positions=e.positions())
        net = inv.net_for_event(["mkt_A", "mkt_B"])
        assert net == pytest.approx(15.0)
