#!/usr/bin/env python3
"""Test all Python code snippets from docs/markov.mdx, docs/monitoring.mdx,
and docs/persistence.mdx.

Prints PASS/FAIL for each snippet. Does NOT fix code -- just reports.
"""

import sys
import traceback
import math
import time
import os

results = []


def run_test(name: str, fn):
    """Run a test function and 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}")
        print()


# ============================================================================
# MARKOV.MDX SNIPPETS
# ============================================================================

# --- Snippet 1: Pre-Trained Model (Quick Start) ---
def test_markov_pretrained_model():
    """markov.mdx - Pre-Trained Model (constructor + fit + markov_regime factory)"""
    import horizon as hz

    # 1. Train on historical log returns
    model = hz.MarkovRegimeModel(n_states=2)
    # Generate synthetic returns
    historical_returns = [0.01 * ((-1) ** i) + 0.001 * i for i in range(50)]
    model.fit(historical_returns, max_iters=100)

    # 2. Verify markov_regime factory works with model=model, feed="book"
    fn = hz.markov_regime(model=model, feed="book")
    assert callable(fn), "markov_regime should return a callable"

run_test("markov.mdx: Pre-Trained Model (Quick Start)", test_markov_pretrained_model)


# --- Snippet 2: Auto-Train Mode ---
def test_markov_autotrain():
    """markov.mdx - Auto-Train Mode (n_states + warmup)"""
    import horizon as hz

    fn = hz.markov_regime(n_states=2, warmup=200, feed="book")
    assert callable(fn), "markov_regime with auto-train should return a callable"

run_test("markov.mdx: Auto-Train Mode", test_markov_autotrain)


# --- Snippet 3: MarkovRegimeModel Constructor ---
def test_markov_constructor():
    """markov.mdx - MarkovRegimeModel(n_states=2)"""
    import horizon as hz

    model = hz.MarkovRegimeModel(n_states=2)
    assert model is not None

run_test("markov.mdx: MarkovRegimeModel Constructor", test_markov_constructor)


# --- Snippet 4: fit() ---
def test_markov_fit():
    """markov.mdx - model.fit(data, max_iters=100, tol=1e-6) returns log-likelihood"""
    import horizon as hz

    model = hz.MarkovRegimeModel(n_states=2)
    data = [0.01, -0.005, 0.02, -0.01, 0.015, -0.008, 0.005, 0.01, -0.02, 0.03,
            0.001, -0.003, 0.007, -0.01, 0.02, -0.015, 0.005, 0.01, -0.005, 0.015]
    log_likelihood = model.fit(data, max_iters=100, tol=1e-6)
    assert isinstance(log_likelihood, float), f"fit() should return float, got {type(log_likelihood)}"

run_test("markov.mdx: fit() returns log-likelihood", test_markov_fit)


# --- Snippet 5: decode() ---
def test_markov_decode():
    """markov.mdx - model.decode(data) returns list of state indices"""
    import horizon as hz

    model = hz.MarkovRegimeModel(n_states=2)
    data = [0.01, -0.005, 0.02, -0.01, 0.015, -0.008, 0.005, 0.01, -0.02, 0.03,
            0.001, -0.003, 0.007, -0.01, 0.02, -0.015, 0.005, 0.01, -0.005, 0.015]
    model.fit(data, max_iters=100)
    states = model.decode(data)
    assert isinstance(states, list), f"decode() should return list, got {type(states)}"
    assert all(isinstance(s, int) for s in states), "All states should be ints"
    assert all(0 <= s < 2 for s in states), "States should be in [0, n_states)"

run_test("markov.mdx: decode() returns state list", test_markov_decode)


# --- Snippet 6: filter_step() ---
def test_markov_filter_step():
    """markov.mdx - model.filter_step(observation) returns list of probs"""
    import horizon as hz

    model = hz.MarkovRegimeModel(n_states=2)
    data = [0.01, -0.005, 0.02, -0.01, 0.015, -0.008, 0.005, 0.01, -0.02, 0.03,
            0.001, -0.003, 0.007, -0.01, 0.02, -0.015, 0.005, 0.01, -0.005, 0.015]
    model.fit(data, max_iters=100)
    probs = model.filter_step(0.01)
    assert isinstance(probs, list), f"filter_step() should return list, got {type(probs)}"
    assert len(probs) == 2, f"Expected 2 probs, got {len(probs)}"
    assert abs(sum(probs) - 1.0) < 1e-6, f"Probs should sum to 1.0, got {sum(probs)}"

run_test("markov.mdx: filter_step() returns probabilities", test_markov_filter_step)


# --- Snippet 7: predict() ---
def test_markov_predict():
    """markov.mdx - model.predict() returns one-step-ahead probs"""
    import horizon as hz

    model = hz.MarkovRegimeModel(n_states=2)
    data = [0.01, -0.005, 0.02, -0.01, 0.015, -0.008, 0.005, 0.01, -0.02, 0.03,
            0.001, -0.003, 0.007, -0.01, 0.02, -0.015, 0.005, 0.01, -0.005, 0.015]
    model.fit(data, max_iters=100)
    model.filter_step(0.01)  # need at least one filter step first
    next_probs = model.predict()
    assert isinstance(next_probs, list), f"predict() should return list, got {type(next_probs)}"
    assert len(next_probs) == 2

run_test("markov.mdx: predict() one-step-ahead", test_markov_predict)


# --- Snippet 8: smooth() ---
def test_markov_smooth():
    """markov.mdx - model.smooth(data) returns list of lists"""
    import horizon as hz

    model = hz.MarkovRegimeModel(n_states=2)
    data = [0.01, -0.005, 0.02, -0.01, 0.015, -0.008, 0.005, 0.01, -0.02, 0.03,
            0.001, -0.003, 0.007, -0.01, 0.02, -0.015, 0.005, 0.01, -0.005, 0.015]
    model.fit(data, max_iters=100)
    smoothed = model.smooth(data)
    assert isinstance(smoothed, list), f"smooth() should return list, got {type(smoothed)}"
    assert len(smoothed) == len(data), f"Expected {len(data)} rows, got {len(smoothed)}"
    for row in smoothed:
        assert len(row) == 2, f"Each row should have 2 probs, got {len(row)}"

run_test("markov.mdx: smooth() forward-backward", test_markov_smooth)


# --- Snippet 9: Other Methods (transition_matrix, emission_params, etc.) ---
def test_markov_other_methods():
    """markov.mdx - transition_matrix(), emission_params(), current_regime(), etc."""
    import horizon as hz

    model = hz.MarkovRegimeModel(n_states=2)
    data = [0.01, -0.005, 0.02, -0.01, 0.015, -0.008, 0.005, 0.01, -0.02, 0.03,
            0.001, -0.003, 0.007, -0.01, 0.02, -0.015, 0.005, 0.01, -0.005, 0.015]
    model.fit(data, max_iters=100)

    # transition_matrix()
    tm = model.transition_matrix()
    assert isinstance(tm, list) and len(tm) == 2
    for row in tm:
        assert len(row) == 2

    # emission_params()
    params = model.emission_params()
    assert isinstance(params, list) and len(params) == 2
    for mean, var in params:
        assert isinstance(mean, float) and isinstance(var, float)

    # current_regime()
    model.filter_step(0.01)
    regime = model.current_regime()
    assert isinstance(regime, int)

    # filtered_probs()
    fp = model.filtered_probs()
    assert isinstance(fp, list) and len(fp) == 2

    # reset_filter()
    model.reset_filter()  # should not raise

    # n_states()
    assert model.n_states() == 2

    # is_trained()
    assert model.is_trained() is True

run_test("markov.mdx: Other Methods (transition_matrix, emission_params, etc.)", test_markov_other_methods)


# --- Snippet 10: prices_to_returns ---
def test_prices_to_returns():
    """markov.mdx - hz.prices_to_returns(prices) returns log returns"""
    import horizon as hz

    prices = [100.0, 101.0, 99.0, 102.0]
    returns = hz.prices_to_returns(prices)
    assert isinstance(returns, list), f"Expected list, got {type(returns)}"
    assert len(returns) == 3, f"Expected 3 returns, got {len(returns)}"
    # Check approximate values from docs
    # ln(101/100) ~ 0.00995, ln(99/101) ~ -0.02005, ln(102/99) ~ 0.02985
    assert abs(returns[0] - math.log(101.0 / 100.0)) < 1e-4
    assert abs(returns[1] - math.log(99.0 / 101.0)) < 1e-4
    assert abs(returns[2] - math.log(102.0 / 99.0)) < 1e-4

run_test("markov.mdx: prices_to_returns", test_prices_to_returns)


# --- Snippet 11: markov_regime() factory parameters ---
def test_markov_regime_factory():
    """markov.mdx - hz.markov_regime() factory with all params"""
    import horizon as hz

    fn = hz.markov_regime(
        model=None,
        n_states=2,
        warmup=100,
        feed="book",
        param_name="regime",
    )
    assert callable(fn)

run_test("markov.mdx: markov_regime() factory params", test_markov_regime_factory)


# --- Snippet 12: Offline Analysis (full example) ---
def test_markov_offline_analysis():
    """markov.mdx - Offline Analysis example (fit, decode, emission_params, transition_matrix, smooth)"""
    import horizon as hz

    # Generate synthetic returns
    returns = [0.001 * ((-1) ** i) + 0.0001 * i for i in range(50)]

    model = hz.MarkovRegimeModel(n_states=3)
    model.fit(returns, max_iters=200)

    # Decode most likely state sequence
    states = model.decode(returns)
    assert len(states) == len(returns)

    # Inspect learned parameters
    for i, (mean, var) in enumerate(model.emission_params()):
        # docs: print(f"State {i}: mean={mean:.6f}, std={var**0.5:.6f}")
        assert isinstance(mean, float)
        assert isinstance(var, float)

    # Transition matrix
    tm = model.transition_matrix()
    for row in tm:
        assert len(row) == 3

    # Smooth for full posterior
    smoothed = model.smooth(returns)
    assert len(smoothed) == len(returns)

run_test("markov.mdx: Offline Analysis example", test_markov_offline_analysis)


# ============================================================================
# MONITORING.MDX SNIPPETS
# ============================================================================

# --- Snippet 13: MetricsCollector import ---
def test_monitoring_metrics_collector_import():
    """monitoring.mdx - from horizon.monitoring import MetricsCollector"""
    try:
        from horizon.monitoring import MetricsCollector
    except ImportError:
        # Docs say horizon.monitoring, actual might be horizon.metrics
        from horizon.metrics import MetricsCollector
        raise ImportError(
            "Docs use 'from horizon.monitoring import MetricsCollector' "
            "but actual module is 'horizon.metrics'"
        )

run_test("monitoring.mdx: import MetricsCollector from horizon.monitoring", test_monitoring_metrics_collector_import)


# --- Snippet 14: Counter ---
def test_monitoring_counter():
    """monitoring.mdx - collector.counter() with .inc()"""
    from horizon.metrics import MetricsCollector

    collector = MetricsCollector()
    orders_counter = collector.counter("horizon_orders_submitted_total")
    orders_counter.inc()
    orders_counter.inc()
    assert orders_counter.value == 2.0, f"Expected 2.0, got {orders_counter.value}"

run_test("monitoring.mdx: Counter .inc()", test_monitoring_counter)


# --- Snippet 15: Gauge ---
def test_monitoring_gauge():
    """monitoring.mdx - collector.gauge() with .set() and .inc()"""
    from horizon.metrics import MetricsCollector

    collector = MetricsCollector()
    position_gauge = collector.gauge("horizon_position_size")
    position_gauge.set(50.0)
    position_gauge.inc()
    assert position_gauge.value == 51.0, f"Expected 51.0, got {position_gauge.value}"

run_test("monitoring.mdx: Gauge .set() and .inc()", test_monitoring_gauge)


# --- Snippet 16: Histogram ---
def test_monitoring_histogram():
    """monitoring.mdx - collector.histogram() with .observe()"""
    from horizon.metrics import MetricsCollector

    collector = MetricsCollector()
    latency_hist = collector.histogram("horizon_tick_latency_seconds")
    latency_hist.observe(0.0032)
    latency_hist.observe(0.0041)
    latency_hist.observe(0.0028)
    # Should not raise

run_test("monitoring.mdx: Histogram .observe()", test_monitoring_histogram)


# --- Snippet 17: Rendering ---
def test_monitoring_render():
    """monitoring.mdx - collector.render() produces Prometheus text format"""
    from horizon.metrics import MetricsCollector

    collector = MetricsCollector()
    orders_counter = collector.counter("horizon_orders_submitted_total")
    orders_counter.inc()
    orders_counter.inc()
    position_gauge = collector.gauge("horizon_position_size")
    position_gauge.set(50.0)
    position_gauge.inc()
    latency_hist = collector.histogram("horizon_tick_latency_seconds")
    latency_hist.observe(0.0032)
    latency_hist.observe(0.0041)
    latency_hist.observe(0.0028)

    output = collector.render()
    assert isinstance(output, str), f"render() should return str, got {type(output)}"
    assert "horizon_orders_submitted_total" in output
    assert "horizon_position_size" in output
    assert "horizon_tick_latency_seconds" in output

run_test("monitoring.mdx: render() Prometheus format", test_monitoring_render)


# --- Snippet 18: MetricsServer import ---
def test_monitoring_metrics_server_import():
    """monitoring.mdx - from horizon.monitoring import MetricsCollector, MetricsServer"""
    try:
        from horizon.monitoring import MetricsCollector, MetricsServer
    except ImportError:
        from horizon.metrics import MetricsCollector, MetricsServer
        raise ImportError(
            "Docs use 'from horizon.monitoring import ...' "
            "but actual module is 'horizon.metrics'"
        )

run_test("monitoring.mdx: import MetricsServer from horizon.monitoring", test_monitoring_metrics_server_import)


# --- Snippet 19: MetricsServer start/stop ---
def test_monitoring_server_start_stop():
    """monitoring.mdx - MetricsServer(collector, port=9090).start() / .stop()"""
    from horizon.metrics import MetricsCollector, MetricsServer

    collector = MetricsCollector()
    # Use a non-standard port to avoid conflicts
    server = MetricsServer(collector, port=19876)
    server.start()
    time.sleep(0.1)
    server.stop()

run_test("monitoring.mdx: MetricsServer start/stop", test_monitoring_server_start_stop)


# --- Snippet 20: update_engine_metrics import ---
def test_monitoring_update_engine_metrics_import():
    """monitoring.mdx - from horizon.monitoring import MetricsCollector, update_engine_metrics"""
    try:
        from horizon.monitoring import MetricsCollector, update_engine_metrics
    except ImportError:
        from horizon.metrics import MetricsCollector, update_engine_metrics
        raise ImportError(
            "Docs use 'from horizon.monitoring import ...' "
            "but actual module is 'horizon.metrics'"
        )

run_test("monitoring.mdx: import update_engine_metrics from horizon.monitoring", test_monitoring_update_engine_metrics_import)


# --- Snippet 21: update_engine_metrics usage ---
def test_monitoring_update_engine_metrics_usage():
    """monitoring.mdx - update_engine_metrics(collector, engine)"""
    from horizon.metrics import MetricsCollector, update_engine_metrics
    import horizon as hz

    collector = MetricsCollector()
    engine = hz.Engine()
    update_engine_metrics(collector, engine)
    # Docs say these gauges are set:
    # horizon_engine_active_orders, horizon_engine_total_fills, etc.
    # Actual code uses: horizon_open_orders, horizon_active_positions, etc.
    output = collector.render()
    # Check for the ACTUAL gauge names
    assert "horizon_open_orders" in output or "horizon_engine_active_orders" in output, \
        f"Expected engine metrics in output. Got:\n{output[:500]}"

run_test("monitoring.mdx: update_engine_metrics usage", test_monitoring_update_engine_metrics_usage)


# --- Snippet 22: AlertManager, AlertType, AlertLevel imports ---
def test_monitoring_alert_imports():
    """monitoring.mdx - from horizon.monitoring import AlertManager, AlertType, AlertLevel"""
    try:
        from horizon.monitoring import AlertManager, AlertType, AlertLevel
    except ImportError as e:
        raise ImportError(
            f"Docs use 'from horizon.monitoring import AlertManager, AlertType, AlertLevel' "
            f"but import failed: {e}. "
            f"Actual module is 'horizon.alerts' and AlertLevel is in horizon._horizon"
        )

run_test("monitoring.mdx: import AlertManager, AlertType, AlertLevel from horizon.monitoring", test_monitoring_alert_imports)


# --- Snippet 23: Alert Channels (WebhookChannel) ---
def test_monitoring_webhook_channel():
    """monitoring.mdx - from horizon.monitoring import WebhookChannel"""
    try:
        from horizon.monitoring import WebhookChannel
        channel = WebhookChannel(url="https://your-server.com/alerts")
    except ImportError:
        from horizon.alerts import WebhookChannel
        channel = WebhookChannel(url="https://your-server.com/alerts")
        raise ImportError(
            "Docs use 'from horizon.monitoring import WebhookChannel' "
            "but actual module is 'horizon.alerts'"
        )

run_test("monitoring.mdx: WebhookChannel import from horizon.monitoring", test_monitoring_webhook_channel)


# --- Snippet 24: TelegramChannel ---
def test_monitoring_telegram_channel():
    """monitoring.mdx - from horizon.monitoring import TelegramChannel"""
    try:
        from horizon.monitoring import TelegramChannel
        channel = TelegramChannel(
            bot_token="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11",
            chat_id="-1001234567890",
        )
    except ImportError:
        from horizon.alerts import TelegramChannel
        channel = TelegramChannel(
            bot_token="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11",
            chat_id="-1001234567890",
        )
        raise ImportError(
            "Docs use 'from horizon.monitoring import TelegramChannel' "
            "but actual module is 'horizon.alerts'"
        )

run_test("monitoring.mdx: TelegramChannel import from horizon.monitoring", test_monitoring_telegram_channel)


# --- Snippet 25: DiscordChannel ---
def test_monitoring_discord_channel():
    """monitoring.mdx - from horizon.monitoring import DiscordChannel"""
    try:
        from horizon.monitoring import DiscordChannel
        channel = DiscordChannel(
            webhook_url="https://discord.com/api/webhooks/1234567890/abcdef",
        )
    except ImportError:
        from horizon.alerts import DiscordChannel
        channel = DiscordChannel(
            webhook_url="https://discord.com/api/webhooks/1234567890/abcdef",
        )
        raise ImportError(
            "Docs use 'from horizon.monitoring import DiscordChannel' "
            "but actual module is 'horizon.alerts'"
        )

run_test("monitoring.mdx: DiscordChannel import from horizon.monitoring", test_monitoring_discord_channel)


# --- Snippet 26: LogChannel ---
def test_monitoring_log_channel():
    """monitoring.mdx - from horizon.monitoring import LogChannel"""
    try:
        from horizon.monitoring import LogChannel
        channel = LogChannel()
    except ImportError:
        from horizon.alerts import LogChannel
        channel = LogChannel()
        raise ImportError(
            "Docs use 'from horizon.monitoring import LogChannel' "
            "but actual module is 'horizon.alerts'"
        )

run_test("monitoring.mdx: LogChannel import from horizon.monitoring", test_monitoring_log_channel)


# --- Snippet 27: AlertType enum values (docs: PriceMove, PositionLimit, etc.) ---
def test_monitoring_alert_type_values():
    """monitoring.mdx - AlertType.PriceMove, PositionLimit, RiskViolation, KillSwitch, FillReceived, Custom"""
    from horizon.alerts import AlertType

    # Docs claim these members exist:
    expected = ["PriceMove", "PositionLimit", "RiskViolation", "KillSwitch", "FillReceived", "Custom"]
    missing = []
    for name in expected:
        if not hasattr(AlertType, name):
            missing.append(name)
    if missing:
        actual_members = [m.name for m in AlertType]
        raise AttributeError(
            f"AlertType missing members from docs: {missing}. "
            f"Actual members: {actual_members}"
        )

run_test("monitoring.mdx: AlertType enum values match docs", test_monitoring_alert_type_values)


# --- Snippet 28: AlertLevel enum values (docs: Info, Warning, Critical) ---
def test_monitoring_alert_level_values():
    """monitoring.mdx - AlertLevel.Info, Warning, Critical"""
    # Docs import from horizon.monitoring
    # AlertLevel is actually in horizon._horizon (Rust enum)
    import horizon as hz

    expected = ["Info", "Warning", "Critical"]
    missing = []
    for name in expected:
        if not hasattr(hz.AlertLevel, name):
            missing.append(name)
    if missing:
        raise AttributeError(
            f"AlertLevel missing members from docs: {missing}"
        )

run_test("monitoring.mdx: AlertLevel enum values (Info, Warning, Critical)", test_monitoring_alert_level_values)


# --- Snippet 29: AlertManager.send() method (docs use .send()) ---
def test_monitoring_alert_manager_send():
    """monitoring.mdx - manager.send(AlertType, message, level=AlertLevel) method"""
    from horizon.alerts import AlertManager, AlertType, LogChannel
    import horizon as hz

    manager = AlertManager()

    # Docs say: manager.add_channel(LogChannel())
    # But actual AlertManager constructor takes channels= param
    # Let's test if add_channel exists
    has_add_channel = hasattr(manager, 'add_channel')

    # Docs say: manager.send(AlertType.FillReceived, "message", level=AlertLevel.Info)
    has_send = hasattr(manager, 'send')

    errors = []
    if not has_add_channel:
        errors.append("AlertManager missing .add_channel() method (docs use it)")
    if not has_send:
        errors.append(
            "AlertManager missing .send() method (docs use it). "
            f"Actual methods: {[m for m in dir(manager) if not m.startswith('_')]}"
        )
    if errors:
        raise AttributeError("; ".join(errors))

run_test("monitoring.mdx: AlertManager.send() and .add_channel() methods", test_monitoring_alert_manager_send)


# --- Snippet 30: Cooldown throttling (set_cooldown method) ---
def test_monitoring_set_cooldown():
    """monitoring.mdx - manager.set_cooldown(AlertType.KillSwitch, seconds=5)"""
    from horizon.alerts import AlertManager

    manager = AlertManager()
    has_set_cooldown = hasattr(manager, 'set_cooldown')
    if not has_set_cooldown:
        raise AttributeError(
            "AlertManager missing .set_cooldown() method mentioned in docs. "
            f"Actual methods: {[m for m in dir(manager) if not m.startswith('_')]}"
        )

run_test("monitoring.mdx: AlertManager.set_cooldown() method", test_monitoring_set_cooldown)


# --- Snippet 31: CalibrationTracker import ---
def test_monitoring_calibration_import():
    """monitoring.mdx - from horizon.monitoring import CalibrationTracker"""
    try:
        from horizon.monitoring import CalibrationTracker
    except ImportError:
        from horizon.calibration import CalibrationTracker
        raise ImportError(
            "Docs use 'from horizon.monitoring import CalibrationTracker' "
            "but actual module is 'horizon.calibration'"
        )

run_test("monitoring.mdx: import CalibrationTracker from horizon.monitoring", test_monitoring_calibration_import)


# --- Snippet 32: CalibrationTracker.record() method ---
def test_monitoring_calibration_record():
    """monitoring.mdx - tracker.record(market_id, predicted_prob, outcome)"""
    from horizon.calibration import CalibrationTracker

    tracker = CalibrationTracker()
    has_record = hasattr(tracker, 'record')
    if not has_record:
        raise AttributeError(
            "CalibrationTracker missing .record() method from docs. "
            f"Actual methods: {[m for m in dir(tracker) if not m.startswith('_')]}"
        )
    # Try calling it as docs show:
    tracker.record(
        market_id="will-it-rain-tomorrow",
        predicted_prob=0.70,
        outcome=1.0,
    )

run_test("monitoring.mdx: CalibrationTracker.record() method", test_monitoring_calibration_record)


# --- Snippet 33: CalibrationTracker.brier_score() ---
def test_monitoring_calibration_brier():
    """monitoring.mdx - tracker.brier_score() returns float"""
    from horizon.calibration import CalibrationTracker

    tracker = CalibrationTracker()
    # Use actual API to add data
    tracker.log_prediction("mkt1", 0.70)
    tracker.resolve_market("mkt1", True)
    score = tracker.brier_score()
    assert isinstance(score, float), f"brier_score() should return float, got {type(score)}"

run_test("monitoring.mdx: CalibrationTracker.brier_score()", test_monitoring_calibration_brier)


# --- Snippet 34: CalibrationTracker.brier_by_market() ---
def test_monitoring_calibration_brier_by_market():
    """monitoring.mdx - tracker.brier_by_market() returns dict"""
    from horizon.calibration import CalibrationTracker

    tracker = CalibrationTracker()
    has_brier_by_market = hasattr(tracker, 'brier_by_market')
    if not has_brier_by_market:
        raise AttributeError(
            "CalibrationTracker missing .brier_by_market() method from docs. "
            f"Actual methods: {[m for m in dir(tracker) if not m.startswith('_')]}"
        )

run_test("monitoring.mdx: CalibrationTracker.brier_by_market()", test_monitoring_calibration_brier_by_market)


# --- Snippet 35: CalibrationTracker.calibration_curve() ---
def test_monitoring_calibration_curve():
    """monitoring.mdx - tracker.calibration_curve(n_bins=10) returns list of (predicted, actual) tuples"""
    from horizon.calibration import CalibrationTracker

    tracker = CalibrationTracker()
    tracker.log_prediction("mkt1", 0.70)
    tracker.resolve_market("mkt1", True)
    tracker.log_prediction("mkt2", 0.30)
    tracker.resolve_market("mkt2", False)

    curve = tracker.calibration_curve(n_bins=10)
    assert isinstance(curve, list), f"calibration_curve should return list, got {type(curve)}"

    # Docs show: for predicted, actual in curve -- tuples of (float, float)
    # Actual returns CalibrationBucket objects
    if len(curve) > 0:
        item = curve[0]
        # Check if it's a tuple (docs) or an object (actual)
        is_tuple = isinstance(item, tuple)
        if not is_tuple:
            # Actual uses CalibrationBucket, not tuples
            raise TypeError(
                f"Docs show calibration_curve() returning list of (predicted, actual) tuples, "
                f"but actual returns list of {type(item).__name__} objects"
            )

run_test("monitoring.mdx: calibration_curve() return type", test_monitoring_calibration_curve)


# --- Snippet 36: Full monitoring imports (Production Setup) ---
def test_monitoring_full_imports():
    """monitoring.mdx - Full production monitoring imports from horizon.monitoring"""
    try:
        from horizon.monitoring import (
            MetricsCollector,
            MetricsServer,
            AlertManager,
            AlertType,
            AlertLevel,
            CalibrationTracker,
            TelegramChannel,
            LogChannel,
            update_engine_metrics,
        )
    except ImportError as e:
        raise ImportError(
            f"Docs use 'from horizon.monitoring import ...' for all monitoring "
            f"components, but import failed: {e}. Components are split across "
            f"horizon.metrics, horizon.alerts, horizon.calibration, horizon._horizon"
        )

run_test("monitoring.mdx: Full production monitoring imports from horizon.monitoring", test_monitoring_full_imports)


# --- Snippet 37: Calibration tracking full example ---
def test_monitoring_calibration_full_example():
    """monitoring.mdx - Full calibration tracking example with record() and brier_by_market()"""
    from horizon.calibration import CalibrationTracker

    tracker = CalibrationTracker()

    predictions = [
        ("market-1", 0.80, 1.0),
        ("market-2", 0.30, 0.0),
        ("market-3", 0.65, 1.0),
        ("market-4", 0.90, 1.0),
        ("market-5", 0.20, 1.0),
        ("market-6", 0.55, 0.0),
        ("market-7", 0.75, 1.0),
        ("market-8", 0.40, 0.0),
        ("market-9", 0.85, 1.0),
        ("market-10", 0.10, 0.0),
    ]

    # Docs use: tracker.record(market_id, prob, outcome)
    # Actual API uses: tracker.log_prediction() + tracker.resolve_market()
    errors = []
    for market_id, prob, outcome in predictions:
        try:
            tracker.record(market_id, prob, outcome)
        except (AttributeError, TypeError) as e:
            errors.append(f"record() failed: {e}")
            break

    if errors:
        raise AttributeError(
            f"CalibrationTracker.record() from docs doesn't work: {errors[0]}. "
            f"Actual API uses log_prediction() + resolve_market()"
        )

    # Docs use: tracker.brier_score()
    score = tracker.brier_score()

    # Docs use: tracker.brier_by_market()
    try:
        by_market = tracker.brier_by_market()
    except AttributeError:
        raise AttributeError(
            "CalibrationTracker.brier_by_market() from docs doesn't exist. "
            f"Actual methods: {[m for m in dir(tracker) if not m.startswith('_')]}"
        )

run_test("monitoring.mdx: Full calibration tracking example", test_monitoring_calibration_full_example)


# --- Snippet 38: hz.backtest with outcomes ---
def test_monitoring_backtest_with_outcomes():
    """monitoring.mdx - hz.backtest(outcomes={"test-market": 1.0}) with Brier score"""
    import horizon as hz

    # Check if backtest accepts outcomes= param
    import inspect
    sig = inspect.signature(hz.backtest)
    has_outcomes = "outcomes" in sig.parameters

    if not has_outcomes:
        raise AttributeError(
            f"hz.backtest() missing 'outcomes' parameter from docs. "
            f"Actual params: {list(sig.parameters.keys())}"
        )

run_test("monitoring.mdx: hz.backtest() outcomes parameter", test_monitoring_backtest_with_outcomes)


# ============================================================================
# PERSISTENCE.MDX SNIPPETS
# ============================================================================

# --- Snippet 39: hz.run with db_path ---
def test_persistence_run_db_path():
    """persistence.mdx - hz.run(name=..., db_path=...) parameter"""
    import horizon as hz
    import inspect

    sig = inspect.signature(hz.run)
    has_db_path = "db_path" in sig.parameters
    if not has_db_path:
        raise AttributeError(
            f"hz.run() missing 'db_path' parameter from docs. "
            f"Actual params: {list(sig.parameters.keys())}"
        )

run_test("persistence.mdx: hz.run(db_path=...) parameter", test_persistence_run_db_path)


# --- Snippet 40: Engine(db_path=...) constructor ---
def test_persistence_engine_db_path():
    """persistence.mdx - Engine(db_path='./my_strategy.db') constructor"""
    import horizon as hz
    import tempfile
    import os

    # Create a temporary file for the DB
    tmp = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
    tmp_path = tmp.name
    tmp.close()

    try:
        engine = hz.Engine(db_path=tmp_path)
        assert engine is not None
    finally:
        if os.path.exists(tmp_path):
            os.unlink(tmp_path)

run_test("persistence.mdx: Engine(db_path=...) constructor", test_persistence_engine_db_path)


# --- Snippet 41: engine.recover_state() ---
def test_persistence_recover_state():
    """persistence.mdx - engine.recover_state() returns int"""
    import horizon as hz
    import tempfile
    import os

    tmp = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
    tmp_path = tmp.name
    tmp.close()

    try:
        engine = hz.Engine(db_path=tmp_path)
        recovered = engine.recover_state()
        assert isinstance(recovered, int), f"recover_state() should return int, got {type(recovered)}"
    finally:
        if os.path.exists(tmp_path):
            os.unlink(tmp_path)

run_test("persistence.mdx: engine.recover_state()", test_persistence_recover_state)


# --- Snippet 42: engine.db_open_order_ids() ---
def test_persistence_db_open_order_ids():
    """persistence.mdx - engine.db_open_order_ids() returns list[str]"""
    import horizon as hz
    import tempfile
    import os

    tmp = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
    tmp_path = tmp.name
    tmp.close()

    try:
        engine = hz.Engine(db_path=tmp_path)
        orphaned = engine.db_open_order_ids()
        assert isinstance(orphaned, list), f"db_open_order_ids should return list, got {type(orphaned)}"
    finally:
        if os.path.exists(tmp_path):
            os.unlink(tmp_path)

run_test("persistence.mdx: engine.db_open_order_ids()", test_persistence_db_open_order_ids)


# --- Snippet 43: engine.snapshot_positions() ---
def test_persistence_snapshot_positions():
    """persistence.mdx - engine.snapshot_positions() returns int"""
    import horizon as hz
    import tempfile
    import os

    tmp = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
    tmp_path = tmp.name
    tmp.close()

    try:
        engine = hz.Engine(db_path=tmp_path)
        count = engine.snapshot_positions()
        assert isinstance(count, int), f"snapshot_positions() should return int, got {type(count)}"
    finally:
        if os.path.exists(tmp_path):
            os.unlink(tmp_path)

run_test("persistence.mdx: engine.snapshot_positions()", test_persistence_snapshot_positions)


# --- Snippet 44: engine.start_run() / engine.end_run() ---
def test_persistence_start_end_run():
    """persistence.mdx - engine.start_run('my_strategy') and engine.end_run()"""
    import horizon as hz
    import tempfile
    import os

    tmp = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
    tmp_path = tmp.name
    tmp.close()

    try:
        engine = hz.Engine(db_path=tmp_path)
        engine.start_run("my_strategy")
        engine.end_run()
    finally:
        if os.path.exists(tmp_path):
            os.unlink(tmp_path)

run_test("persistence.mdx: engine.start_run() / end_run()", test_persistence_start_end_run)


# --- Snippet 45: engine.has_persistence() ---
def test_persistence_has_persistence():
    """persistence.mdx - engine.has_persistence() returns True/False"""
    import horizon as hz
    import tempfile
    import os

    tmp = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
    tmp_path = tmp.name
    tmp.close()

    try:
        engine = hz.Engine(db_path=tmp_path)
        result = engine.has_persistence()
        assert isinstance(result, bool), f"has_persistence() should return bool, got {type(result)}"
        assert result is True, "Engine with db_path should have persistence=True"
    finally:
        if os.path.exists(tmp_path):
            os.unlink(tmp_path)

run_test("persistence.mdx: engine.has_persistence()", test_persistence_has_persistence)


# --- Snippet 46: engine.db_run_id() ---
def test_persistence_db_run_id():
    """persistence.mdx - engine.db_run_id() returns UUID string"""
    import horizon as hz
    import tempfile
    import os

    tmp = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
    tmp_path = tmp.name
    tmp.close()

    try:
        engine = hz.Engine(db_path=tmp_path)
        engine.start_run("my_strategy")
        run_id = engine.db_run_id()
        # Should be a UUID string (or None if no run started)
        assert run_id is None or isinstance(run_id, str), \
            f"db_run_id() should return str or None, got {type(run_id)}"
        engine.end_run()
    finally:
        if os.path.exists(tmp_path):
            os.unlink(tmp_path)

run_test("persistence.mdx: engine.db_run_id()", test_persistence_db_run_id)


# --- Snippet 47: Engine without persistence (no db_path) ---
def test_persistence_no_db():
    """persistence.mdx - Engine without persistence: has_persistence() returns False"""
    import horizon as hz

    engine = hz.Engine()
    result = engine.has_persistence()
    assert result is False, f"Engine without db_path should have persistence=False, got {result}"

run_test("persistence.mdx: Engine without db_path has no persistence", test_persistence_no_db)


# ============================================================================
# SUMMARY
# ============================================================================

print("\n" + "=" * 70)
print("SUMMARY")
print("=" * 70)

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

print(f"\nTotal: {total}")
print(f"PASS:  {passed}")
print(f"FAIL:  {failed}")
print()

if failed > 0:
    print("FAILED TESTS:")
    print("-" * 70)
    for name, status, tb in results:
        if status == "FAIL":
            print(f"\n  {name}")
            # Print just the last line of the traceback (the error message)
            lines = tb.strip().split("\n")
            for line in lines[-3:]:
                print(f"    {line}")
    print()

sys.exit(0 if failed == 0 else 1)
