"""
Test every Python code snippet from the core-concepts docs.

Source files:
  1. docs/core-concepts/risk.mdx
  2. docs/core-concepts/context.mdx
  3. docs/core-concepts/pipeline.mdx
  4. docs/core-concepts/hz-run.mdx

Each snippet is tested as-is (no fixes). Reports PASS/FAIL for each.
"""

import sys
import traceback

import horizon as hz
from horizon import (
    Engine,
    EngineStatus,
    Event,
    Market,
    Quote,
    RiskConfig,
    Side,
)
from horizon.context import Context, FeedData, InventorySnapshot

results = []


def run_test(name, fn):
    """Run a test function; record PASS/FAIL."""
    try:
        fn()
        results.append((name, "PASS", ""))
        print(f"  PASS: {name}")
    except Exception as e:
        tb = traceback.format_exc()
        results.append((name, "FAIL", tb))
        print(f"  FAIL: {name}")
        print(f"        {e}")


# ---------------------------------------------------------------------------
# Helpers: Build objects needed by doc snippets
# ---------------------------------------------------------------------------

def _make_engine(**kwargs):
    """Build a paper Engine (no persistence)."""
    return Engine(db_path=None, **kwargs)


def _make_context(**overrides):
    """Build a minimal Context with sane defaults."""
    defaults = dict(
        feeds={"btc": FeedData(price=50000.0, bid=49990.0, ask=50010.0, timestamp=1e9)},
        inventory=InventorySnapshot(positions=[]),
        market=Market(id="test-market", name="test-market", slug="test-market"),
        status=None,
        params={},
    )
    defaults.update(overrides)
    return Context(**defaults)


# ============================================================================
# FILE 1: risk.mdx
# ============================================================================
print("\n=== risk.mdx ===")

# Snippet 1: Risk Builder inside hz.run() — just test hz.Risk() creation
def test_risk_builder():
    """risk.mdx snippet 1: Risk builder (hz.Risk(...))"""
    risk = hz.Risk(
        max_position=100,
        max_notional=1000,
        max_drawdown_pct=5,
        max_order_size=50,
        rate_limit=50,
        rate_burst=300,
    )
    # Verify it converts to RiskConfig
    cfg = risk.to_config()
    assert isinstance(cfg, RiskConfig)

run_test("risk.mdx #1: Risk builder", test_risk_builder)


# Snippet 2: RiskConfig (Direct)
def test_risk_config_direct():
    """risk.mdx snippet 2: RiskConfig direct creation"""
    from horizon import RiskConfig

    config = RiskConfig(
        max_position_per_market=100.0,
        max_portfolio_notional=1000.0,
        max_daily_drawdown_pct=5.0,
        max_order_size=50.0,
        rate_limit_sustained=50,
        rate_limit_burst=300,
        dedup_window_ms=1000,
    )
    assert isinstance(config, RiskConfig)

run_test("risk.mdx #2: RiskConfig direct", test_risk_config_direct)


# Snippet 3: Kill switch
def test_kill_switch():
    """risk.mdx snippet 3: Kill switch"""
    engine = _make_engine()

    # Activate: blocks all orders + cancels existing
    engine.activate_kill_switch("manual halt")

    # Deactivate: resume trading
    engine.deactivate_kill_switch()

    # Check status
    status = engine.status()
    if status.kill_switch_active:
        print(f"Kill switch reason: {status.kill_switch_reason}")

run_test("risk.mdx #3: Kill switch", test_kill_switch)


# Snippet 4: Drawdown tracking
def test_drawdown_tracking():
    """risk.mdx snippet 4: Drawdown tracking"""
    engine = _make_engine()

    # Manually set the daily baseline
    engine.set_daily_baseline(1000.0)

    # Update P&L (done automatically in the main loop)
    current_pnl = 990.0
    engine.update_daily_pnl(current_pnl)

run_test("risk.mdx #4: Drawdown tracking", test_drawdown_tracking)


# Snippet 5: Event risk limits (RiskConfig with max_position_per_event)
def test_event_risk_limits():
    """risk.mdx snippet 5: Event risk limits"""
    config = RiskConfig(
        max_position_per_market=100.0,
        max_position_per_event=200.0,  # Caps total across all outcomes
    )
    assert isinstance(config, RiskConfig)

run_test("risk.mdx #5: Event risk limits", test_event_risk_limits)


# ============================================================================
# FILE 2: context.mdx
# ============================================================================
print("\n=== context.mdx ===")

# Snippet 1: Context dataclass
def test_context_structure():
    """context.mdx snippet 1: Context structure (dataclass definition)"""
    # Verify the @dataclass fields match the docs
    from dataclasses import fields as dc_fields
    field_names = {f.name for f in dc_fields(Context)}
    expected = {"feeds", "inventory", "market", "event", "status", "params"}
    assert expected == field_names, f"Expected {expected}, got {field_names}"

run_test("context.mdx #1: Context structure", test_context_structure)


# Snippet 2: FeedData dataclass
def test_feed_data_structure():
    """context.mdx snippet 2: FeedData dataclass"""
    # The doc shows:
    #   @dataclass
    #   class FeedData:
    #       price: float = 0.0
    #       timestamp: float = 0.0
    #       bid: float = 0.0
    #       ask: float = 0.0
    fd = FeedData()
    assert fd.price == 0.0
    assert fd.timestamp == 0.0
    assert fd.bid == 0.0
    assert fd.ask == 0.0

run_test("context.mdx #2: FeedData dataclass", test_feed_data_structure)


# Snippet 3: Accessing feeds
def test_accessing_feeds():
    """context.mdx snippet 3: Accessing feeds"""
    ctx = _make_context()

    def fair_value(ctx: hz.Context) -> float:
        btc = ctx.feeds.get("btc", hz.context.FeedData())
        return btc.price  # Latest price from feed

    result = fair_value(ctx)
    assert result == 50000.0

run_test("context.mdx #3: Accessing feeds", test_accessing_feeds)


# Snippet 4: Feed staleness
def test_feed_staleness():
    """context.mdx snippet 4: Feed staleness check"""
    feed = FeedData()  # timestamp=0 -> stale
    assert feed.is_stale(max_age_secs=30.0) is True

    feed2 = FeedData(timestamp=1e18)  # far future -> not stale
    assert feed2.is_stale(max_age_secs=30.0) is False

run_test("context.mdx #4: Feed staleness", test_feed_staleness)


# Snippet 5: Feed staleness via params (just verifying params can be set)
def test_feed_stale_threshold_param():
    """context.mdx snippet 5: feed_stale_threshold param"""
    # Docs show: hz.run(params={"feed_stale_threshold": 60.0}, ...)
    # We just verify the param can be passed to context
    ctx = _make_context(params={"feed_stale_threshold": 60.0})
    assert ctx.params["feed_stale_threshold"] == 60.0

run_test("context.mdx #5: feed_stale_threshold param", test_feed_stale_threshold_param)


# Snippet 6: InventorySnapshot dataclass
def test_inventory_snapshot_structure():
    """context.mdx snippet 6: InventorySnapshot structure"""
    inv = InventorySnapshot(positions=[])
    assert inv.net == 0.0
    assert inv.net_for_market("some-market") == 0.0
    assert inv.net_for_event(["a", "b"]) == 0.0
    assert inv.positions_for_event(["a", "b"]) == []

run_test("context.mdx #6: InventorySnapshot structure", test_inventory_snapshot_structure)


# Snippet 7: Inventory-based quoting
def test_inventory_based_quoting():
    """context.mdx snippet 7: Inventory-based quoting"""
    ctx = _make_context()

    def quoter(ctx: hz.Context, fair: float) -> list[hz.Quote]:
        # Skew quotes based on inventory
        net_pos = ctx.inventory.net
        skew = net_pos * 0.001
        return hz.quotes(fair - skew, spread=0.04, size=5)

    quotes = quoter(ctx, 0.50)
    assert len(quotes) == 1
    assert isinstance(quotes[0], Quote)

run_test("context.mdx #7: Inventory-based quoting", test_inventory_based_quoting)


# Snippet 8: Per-market inventory
def test_per_market_inventory():
    """context.mdx snippet 8: Per-market inventory"""
    ctx = _make_context()

    def quoter(ctx: hz.Context, fair: float) -> list[hz.Quote]:
        market_pos = ctx.inventory.net_for_market(ctx.market.id)
        # Reduce size when position is large
        size = max(1, 10 - abs(market_pos) * 0.1)
        return hz.quotes(fair, spread=0.04, size=size)

    quotes = quoter(ctx, 0.50)
    assert len(quotes) == 1

run_test("context.mdx #8: Per-market inventory", test_per_market_inventory)


# Snippet 9: Market-based quoting (ctx.market.exchange)
def test_market_exchange_quoting():
    """context.mdx snippet 9: Market exchange quoting"""
    ctx = _make_context()

    def quoter(ctx: hz.Context, fair: float) -> list[hz.Quote]:
        if ctx.market.exchange == "kalshi":
            spread = 0.04  # Wider spread for Kalshi
        else:
            spread = 0.02
        return hz.quotes(fair, spread, size=5)

    quotes = quoter(ctx, 0.50)
    assert len(quotes) == 1

run_test("context.mdx #9: Market exchange quoting", test_market_exchange_quoting)


# Snippet 10: Event quoting (ctx.event)
def test_event_quoting():
    """context.mdx snippet 10: Event quoting"""
    ctx = _make_context(event=None)

    def quoter(ctx: hz.Context, fair: float) -> list[hz.Quote]:
        if ctx.event:
            # Access event info
            event_market_ids = [o.market_id for o in ctx.event.outcomes]
            event_pos = ctx.inventory.net_for_event(event_market_ids)
            skew = event_pos * 0.001
            return hz.quotes(fair - skew, spread=0.04, size=5)
        return hz.quotes(fair, spread=0.04, size=5)

    quotes = quoter(ctx, 0.50)
    assert len(quotes) == 1

run_test("context.mdx #10: Event quoting", test_event_quoting)


# Snippet 11: EngineStatus quoting
def test_engine_status_quoting():
    """context.mdx snippet 11: EngineStatus quoting"""
    engine = _make_engine()
    status = engine.status()
    ctx = _make_context(status=status)

    def quoter(ctx: hz.Context, fair: float) -> list[hz.Quote]:
        if ctx.status.kill_switch_active:
            return []  # Don't quote when kill switch is on

        # Reduce size as drawdown increases
        drawdown = -ctx.status.daily_pnl
        size = max(1, 10 - drawdown * 0.5)
        return hz.quotes(fair, spread=0.04, size=size)

    quotes = quoter(ctx, 0.50)
    # Kill switch is not active, so we should get quotes
    assert len(quotes) >= 1

run_test("context.mdx #11: EngineStatus quoting", test_engine_status_quoting)


# Snippet 12: Custom parameters
def test_custom_params():
    """context.mdx snippet 12: Custom parameters"""
    ctx = _make_context(params={
        "gamma": 0.1,
        "base_spread": 0.02,
        "max_inventory": 50,
    })

    def quoter(ctx: hz.Context, fair: float) -> list[hz.Quote]:
        gamma = ctx.params["gamma"]
        spread = ctx.params["base_spread"]
        max_inv = ctx.params["max_inventory"]

        # Widen spread as inventory approaches limit
        inv_ratio = abs(ctx.inventory.net) / max_inv
        adjusted_spread = spread * (1 + gamma * inv_ratio)

        return hz.quotes(fair, adjusted_spread, size=5)

    quotes = quoter(ctx, 0.50)
    assert len(quotes) == 1

run_test("context.mdx #12: Custom parameters", test_custom_params)


# ============================================================================
# FILE 3: pipeline.mdx
# ============================================================================
print("\n=== pipeline.mdx ===")

# Snippet 1: Basic pipeline example
def test_basic_pipeline():
    """pipeline.mdx snippet 1: Basic pipeline (fair_value, toxicity, quoter)"""
    # Step 1: Just context, compute fair value
    def fair_value(ctx: hz.Context) -> float:
        return ctx.feeds["btc"].price * 0.01

    # Step 2: Context + previous output, compute toxicity
    def toxicity(ctx: hz.Context) -> float:
        return 0.5  # VPIN, Kyle's lambda, etc.

    # Step 3: Context + two previous outputs, generate quotes
    def quoter(ctx: hz.Context, fair: float, tox: float) -> list[hz.Quote]:
        spread = 0.02 + tox * 0.04
        return hz.quotes(fair, spread, size=5)

    # Test by running manually
    ctx = _make_context()
    fv = fair_value(ctx)
    tox = toxicity(ctx)
    quotes = quoter(ctx, fv, tox)
    assert isinstance(fv, float)
    assert tox == 0.5
    assert len(quotes) == 1

run_test("pipeline.mdx #1: Basic pipeline", test_basic_pipeline)


# Snippet 2: Signature introspection
def test_signature_introspection():
    """pipeline.mdx snippet 2: Signature introspection"""
    import inspect

    def fn1(ctx): return 1.0
    def fn2(ctx, prev): return prev * 2
    def fn3(ctx, a, b): return a + b

    pipeline = [fn1, fn2, fn3]

    for fn in pipeline:
        sig = inspect.signature(fn)
        n_params = len(sig.parameters)
        # n_params == 1: fn(ctx)
        # n_params == 2: fn(ctx, prev_output)
        # n_params == 3: fn(ctx, output_n-2, output_n-1)

    assert len(inspect.signature(fn1).parameters) == 1
    assert len(inspect.signature(fn2).parameters) == 2
    assert len(inspect.signature(fn3).parameters) == 3

run_test("pipeline.mdx #2: Signature introspection", test_signature_introspection)


# Snippet 3: Adding a stage (inventory_skew)
def test_adding_stage():
    """pipeline.mdx snippet 3: Adding a stage (inventory_skew)"""
    def fair_value(ctx: hz.Context) -> float:
        return 0.50

    def inventory_skew(ctx: hz.Context, fair: float) -> float:
        """Skew the fair value based on inventory."""
        skew = ctx.inventory.net * 0.001
        return fair - skew

    def quoter(ctx: hz.Context, fair: float) -> list[hz.Quote]:
        return hz.quotes(fair, spread=0.04, size=5)

    ctx = _make_context()
    fv = fair_value(ctx)
    skewed = inventory_skew(ctx, fv)
    quotes = quoter(ctx, skewed)
    assert len(quotes) == 1

run_test("pipeline.mdx #3: Adding a stage", test_adding_stage)


# Snippet 4: Multiple data sources
def test_multiple_data_sources():
    """pipeline.mdx snippet 4: Multiple data sources"""
    ctx = _make_context(feeds={
        "poly": FeedData(price=0.55, bid=0.54, ask=0.56, timestamp=1e9),
        "kalshi": FeedData(price=0.53, bid=0.52, ask=0.54, timestamp=1e9),
        "btc": FeedData(price=50000.0, bid=49990.0, ask=50010.0, timestamp=1e9),
    })

    def fair_value(ctx: hz.Context) -> float:
        poly = ctx.feeds.get("poly", hz.context.FeedData())
        kalshi = ctx.feeds.get("kalshi", hz.context.FeedData())
        return (poly.price + kalshi.price) / 2

    def volatility(ctx: hz.Context) -> float:
        btc = ctx.feeds.get("btc", hz.context.FeedData())
        return abs(btc.bid - btc.ask) / max(btc.price, 1)

    def quoter(ctx: hz.Context, fair: float, vol: float) -> list[hz.Quote]:
        spread = 0.02 + vol * 0.1
        return hz.quotes(fair, spread, size=5)

    fv = fair_value(ctx)
    vol = volatility(ctx)
    quotes = quoter(ctx, fv, vol)
    assert abs(fv - 0.54) < 1e-9
    assert len(quotes) == 1

run_test("pipeline.mdx #4: Multiple data sources", test_multiple_data_sources)


# Snippet 5: hz.quotes() helper
def test_hz_quotes_helper():
    """pipeline.mdx snippet 5: hz.quotes() helper"""
    quotes = hz.quotes(fair=0.50, spread=0.06, size=5.0)
    # -> [Quote(bid=0.47, ask=0.53, size=5.0)]
    assert len(quotes) == 1
    q = quotes[0]
    assert abs(q.bid - 0.47) < 1e-9
    assert abs(q.ask - 0.53) < 1e-9
    assert abs(q.size - 5.0) < 1e-9

run_test("pipeline.mdx #5: hz.quotes() helper", test_hz_quotes_helper)


# Snippet 6: Manual Quote creation
def test_manual_quotes():
    """pipeline.mdx snippet 6: Manual Quote creation"""
    quotes = [
        hz.Quote(bid=0.45, ask=0.55, size=10.0),
        hz.Quote(bid=0.40, ask=0.60, size=5.0),
    ]
    assert len(quotes) == 2
    assert isinstance(quotes[0], Quote)
    assert isinstance(quotes[1], Quote)

run_test("pipeline.mdx #6: Manual Quote creation", test_manual_quotes)


# Snippet 7: Signal -> Kelly -> Market Maker (just test the factory calls exist)
def test_signal_kelly_market_maker():
    """pipeline.mdx snippet 7: Signal -> Kelly -> Market Maker factories"""
    # Test that these factory functions can be called without error.
    # We can't call hz.run() (it starts a loop), but we can verify the
    # factories return callables.
    combiner = hz.signal_combiner([
        hz.price_signal("book", weight=0.5),
        hz.momentum_signal("book", lookback=20, weight=0.3),
        hz.spread_signal("book", weight=0.2),
    ], smoothing=10)
    assert callable(combiner)

    mm = hz.market_maker(feed_name="book", gamma=0.5, size=5.0)
    assert callable(mm)

run_test("pipeline.mdx #7: Signal -> Kelly -> Market Maker", test_signal_kelly_market_maker)


# ============================================================================
# FILE 4: hz-run.mdx
# ============================================================================
print("\n=== hz-run.mdx ===")

# Snippet 1: Full signature — can't call hz.run() (starts a loop)
# We test that the function accepts the documented parameters by inspecting it.
def test_hz_run_signature():
    """hz-run.mdx snippet 1: hz.run() full signature"""
    import inspect
    sig = inspect.signature(hz.run)
    param_names = list(sig.parameters.keys())
    expected = [
        "name", "exchange", "exchanges", "markets", "events", "feeds",
        "pipeline", "risk", "interval", "mode", "dashboard", "params",
        "db_path", "netting_pairs", "api_key",
    ]
    for p in expected:
        assert p in param_names, f"Expected parameter '{p}' not found in hz.run() signature"

run_test("hz-run.mdx #1: hz.run() signature", test_hz_run_signature)


# Snippet 2: Paper trading (simplest) — verify no exceptions in setup
# (We can't actually call hz.run because it starts the event loop,
#  but we can verify Risk + pipeline setup)
def test_paper_trading_setup():
    """hz-run.mdx snippet 2: Paper trading (setup only, no loop)"""
    def fair_value(ctx):
        return 0.5

    def quoter(ctx, fair):
        return hz.quotes(fair, spread=0.04, size=5)

    # Verify all components can be built
    risk = hz.Risk(max_position=100)
    pipeline = [fair_value, quoter]
    markets = ["test-market"]
    assert len(pipeline) == 2
    assert risk.max_position == 100

run_test("hz-run.mdx #2: Paper trading setup", test_paper_trading_setup)


# Snippet 3: Live on Polymarket — just verify Polymarket config can be created
def test_polymarket_config():
    """hz-run.mdx snippet 3: Polymarket config"""
    poly = hz.Polymarket(private_key="0xdead")
    assert poly.private_key == "0xdead"

    feed = hz.BinanceWS("btcusdt")
    assert feed.symbol == "btcusdt"

run_test("hz-run.mdx #3: Polymarket config", test_polymarket_config)


# Snippet 4: Multi-outcome events — verify discover_events and Event/Outcome types exist
def test_multi_outcome_types():
    """hz-run.mdx snippet 4: Multi-outcome event types"""
    # We can't call discover_events (it requires network), but verify it exists
    assert callable(hz.discover_events)
    # Verify Risk accepts max_position_per_event (via RiskConfig)
    risk = hz.Risk(max_position=50)
    cfg = risk.to_config()
    assert isinstance(cfg, RiskConfig)

run_test("hz-run.mdx #4: Multi-outcome event types", test_multi_outcome_types)


# Snippet 5: Multi-exchange with netting — verify configs
def test_multi_exchange_config():
    """hz-run.mdx snippet 5: Multi-exchange with netting config"""
    poly = hz.Polymarket(private_key="0xdead")
    kalshi = hz.Kalshi(api_key="test-key")
    feed = hz.BinanceWS("btcusdt")

    exchanges = [poly, kalshi]
    markets = ["btc-100k-poly", "KXBTC-25FEB16"]
    netting_pairs = [("btc-100k-poly", "KXBTC-25FEB16")]

    assert len(exchanges) == 2
    assert len(netting_pairs) == 1

run_test("hz-run.mdx #5: Multi-exchange config", test_multi_exchange_config)


# Snippet 6: Risk builder with max_position_per_event (from hz-run.mdx examples)
def test_risk_max_position_per_event():
    """hz-run.mdx snippet 6: Risk(max_position_per_event=150)"""
    # The docs show: hz.Risk(max_position=50, max_position_per_event=150)
    # Check if Risk accepts max_position_per_event
    try:
        risk = hz.Risk(max_position=50, max_position_per_event=150)
        # If it works, great
    except TypeError as e:
        # Risk builder may not accept max_position_per_event — report as-is
        raise

run_test("hz-run.mdx #6: Risk(max_position_per_event=...)", test_risk_max_position_per_event)


# ============================================================================
# SUMMARY
# ============================================================================
print("\n" + "=" * 60)
print("SUMMARY")
print("=" * 60)

passed = sum(1 for _, status, _ in results if status == "PASS")
failed = sum(1 for _, status, _ in results if status == "FAIL")

for name, status, tb in results:
    print(f"  [{status}] {name}")
    if status == "FAIL":
        # Print condensed traceback
        for line in tb.strip().split("\n"):
            print(f"         {line}")

print(f"\nTotal: {len(results)} | Passed: {passed} | Failed: {failed}")

if failed > 0:
    sys.exit(1)
