"""Deep test of strategy.py: pipeline composition, _process_result, CLI overrides, internals."""
import sys
import inspect

errors = []

def check(name, condition, detail=""):
    if not condition:
        msg = f"FAIL: {name}"
        if detail:
            msg += f" -- {detail}"
        errors.append(msg)
        print(msg)
    else:
        print(f"  OK: {name}")


print("=" * 60)
print("TEST: strategy.py internals")
print("=" * 60)

from horizon.strategy import (
    _get_n_params,
    _run_pipeline,
    _process_result,
    _build_context,
    _cli_overrides,
    _start_feeds,
    _create_engine,
    _resolve_kalshi_markets,
    run,
)
from horizon._horizon import Engine, Market, Quote, Side, RiskConfig
from horizon.context import Context, FeedData, InventorySnapshot
from horizon.risk import Risk
from horizon.feeds import BinanceWS, RESTFeed, PolymarketBook, KalshiBook


# ---------------------------------------------------------------------------
# _get_n_params
# ---------------------------------------------------------------------------
print("\n--- _get_n_params ---")

def fn_one(ctx): return ctx
def fn_two(ctx, prev): return prev
def fn_three(ctx, a, b): return (a, b)
def fn_four(ctx, a, b, c): return c

check("_get_n_params 1", _get_n_params(fn_one) == 1, f"got {_get_n_params(fn_one)}")
check("_get_n_params 2", _get_n_params(fn_two) == 2, f"got {_get_n_params(fn_two)}")
check("_get_n_params 3", _get_n_params(fn_three) == 3, f"got {_get_n_params(fn_three)}")
check("_get_n_params 4", _get_n_params(fn_four) == 4, f"got {_get_n_params(fn_four)}")

# Caching test
_get_n_params(fn_one)
check("_get_n_params caching", hasattr(fn_one, "_hz_nparams"), "cache not set")
check("_get_n_params cache value", fn_one._hz_nparams == 1, f"got {fn_one._hz_nparams}")

# Lambda
lam = lambda ctx: ctx
check("_get_n_params lambda", _get_n_params(lam) == 1)


# ---------------------------------------------------------------------------
# _run_pipeline
# ---------------------------------------------------------------------------
print("\n--- _run_pipeline ---")

ctx = Context(
    feeds={"btc": FeedData(price=0.6, bid=0.58, ask=0.62)},
    market=Market(id="m1", name="M1", slug="m1"),
    params={"bankroll": 1000},
)

# Single function pipeline
def step1(ctx):
    return 0.65

result = _run_pipeline([step1], ctx)
check("pipeline single fn", result == 0.65, f"got {result}")

# Two function pipeline - chaining
def step2(ctx, prob):
    return prob * 2

result = _run_pipeline([step1, step2], ctx)
check("pipeline two fns chained", result == 1.30, f"got {result}")

# Three function pipeline
def step3(ctx, prev1, prev2):
    return prev1 + prev2

result = _run_pipeline([step1, step2, step3], ctx)
check("pipeline three fns", result == 0.65 + 1.30, f"got {result} expected {0.65 + 1.30}")

# Pipeline with None prev_outputs for 2-param fn at start
def two_param_first(ctx, prev):
    return "got_none" if prev is None else "got_value"

result = _run_pipeline([two_param_first], ctx)
check("pipeline 2-param first gets None", result == "got_none", f"got {result}")

# Empty pipeline returns None
result = _run_pipeline([], ctx)
check("pipeline empty returns None", result is None, f"got {result}")

# Pipeline that returns a Quote list
def quoter(ctx, prob):
    half = 0.02
    return [Quote(bid=prob - half, ask=prob + half, size=5.0)]

result = _run_pipeline([step1, quoter], ctx)
check("pipeline returns Quote list", isinstance(result, list), f"got {type(result)}")
check("pipeline Quote list length", len(result) == 1, f"got {len(result)}")
check("pipeline Quote bid", abs(result[0].bid - 0.63) < 0.001, f"got {result[0].bid}")
check("pipeline Quote ask", abs(result[0].ask - 0.67) < 0.001, f"got {result[0].ask}")


# ---------------------------------------------------------------------------
# _process_result
# ---------------------------------------------------------------------------
print("\n--- _process_result ---")

engine = Engine(db_path=None)
engine.set_daily_baseline(0.0)
market = Market(id="test_pr", name="Test PR", slug="test_pr")

# Process a single Quote
q = Quote(bid=0.45, ask=0.55, size=5.0)
_process_result(engine, market, q, ctx)

status = engine.status()
check("_process_result creates orders", status.open_orders > 0 or status.active_positions >= 0,
      f"open={status.open_orders}")

# Process a list of Quotes
q_list = [Quote(bid=0.40, ask=0.60, size=3.0), Quote(bid=0.35, ask=0.65, size=2.0)]
_process_result(engine, market, q_list, ctx)

# Process invalid result
import logging
logging.disable(logging.CRITICAL)
_process_result(engine, market, "invalid", ctx)  # Should not crash
_process_result(engine, market, 42, ctx)  # Should not crash
logging.disable(logging.NOTSET)
check("_process_result handles invalid types", True)  # Didn't crash

# Process None - this should not happen as _run_loop checks for None
# But let's check that _process_result doesn't crash
try:
    _process_result(engine, market, None, ctx)
    check("_process_result None doesn't crash", True)
except Exception as e:
    check("_process_result None doesn't crash", False, str(e))


# ---------------------------------------------------------------------------
# _build_context
# ---------------------------------------------------------------------------
print("\n--- _build_context ---")

engine2 = Engine(db_path=None)
engine2.set_daily_baseline(0.0)
market2 = Market(id="mkt_ctx", name="Market CTX", slug="mkt_ctx")
feeds_config = {"btc": BinanceWS("btcusdt"), "eth": BinanceWS("ethusdt")}

# Can't start feeds without tokio (would need engine.start_feed)
# Instead test with empty feeds
ctx_built = _build_context(engine2, market2, {}, {"alpha": 0.5})

check("_build_context returns Context", isinstance(ctx_built, Context))
check("_build_context market set", ctx_built.market.id == "mkt_ctx")
check("_build_context status set", ctx_built.status is not None)
check("_build_context params has alpha", ctx_built.params.get("alpha") == 0.5)
check("_build_context params has engine", ctx_built.params.get("engine") is not None)
check("_build_context params has _recent_fills", "_recent_fills" in ctx_built.params)
check("_build_context params has fills_this_cycle", "fills_this_cycle" in ctx_built.params)
check("_build_context feeds empty", len(ctx_built.feeds) == 0)


# ---------------------------------------------------------------------------
# _cli_overrides
# ---------------------------------------------------------------------------
print("\n--- _cli_overrides ---")

check("_cli_overrides is dict", isinstance(_cli_overrides, dict))
check("_cli_overrides initially empty", len(_cli_overrides) == 0, f"got {len(_cli_overrides)}")

# Test that overrides affect run() args
_cli_overrides["mode"] = "paper"
_cli_overrides["dashboard"] = False
check("_cli_overrides can be set", _cli_overrides.get("mode") == "paper")
_cli_overrides.clear()


# ---------------------------------------------------------------------------
# _create_engine
# ---------------------------------------------------------------------------
print("\n--- _create_engine ---")

# Paper engine
e_paper = _create_engine([], None, "paper")
check("_create_engine paper", e_paper is not None)
check("_create_engine paper exchanges", "paper" in e_paper.exchange_names())

# Paper engine with risk config
risk_cfg = RiskConfig(max_position_per_market=50.0)
e_risk = _create_engine([], risk_cfg, "paper")
check("_create_engine with risk", e_risk is not None)


# ---------------------------------------------------------------------------
# _resolve_kalshi_markets
# ---------------------------------------------------------------------------
print("\n--- _resolve_kalshi_markets ---")

kalshi_market = Market(id="kxbtc-25feb16", name="BTC", slug="kxbtc-25feb16")
_resolve_kalshi_markets([kalshi_market])
check("_resolve_kalshi_markets ticker set", kalshi_market.ticker == "KXBTC-25FEB16",
      f"got {kalshi_market.ticker}")
check("_resolve_kalshi_markets exchange set", kalshi_market.exchange == "kalshi",
      f"got {kalshi_market.exchange}")


# ---------------------------------------------------------------------------
# run() validation
# ---------------------------------------------------------------------------
print("\n--- run() input validation ---")

# pipeline must not be empty
try:
    run("test", pipeline=[])
    check("run() empty pipeline raises", False)
except ValueError as e:
    check("run() empty pipeline raises", "at least one function" in str(e), str(e))

# interval must be positive
try:
    run("test", pipeline=[lambda ctx: None], interval=-1.0)
    check("run() negative interval raises", False)
except ValueError as e:
    check("run() negative interval raises", "positive" in str(e), str(e))

# pipeline dict must not be empty
try:
    run("test", pipeline={})
    check("run() empty pipeline dict raises", False)
except ValueError as e:
    check("run() empty pipeline dict raises", "must not be empty" in str(e), str(e))

# exchange + exchanges mutually exclusive
from horizon.exchanges import Polymarket, Kalshi
try:
    run("test", exchange=Polymarket(), exchanges=[Kalshi()], pipeline=[lambda ctx: None])
    check("run() exchange+exchanges raises", False)
except ValueError as e:
    check("run() exchange+exchanges raises", "Cannot specify both" in str(e), str(e))


# ---------------------------------------------------------------------------
# Feed wiring test
# ---------------------------------------------------------------------------
print("\n--- Feed configs ---")

# Test all feed config types can be instantiated
from horizon.feeds import (
    PredictItFeed, ManifoldFeed, ESPNFeed, NWSFeed, RESTJsonPathFeed
)

feeds = {
    "binance": BinanceWS("btcusdt"),
    "rest": RESTFeed("https://example.com/price", interval=5.0),
    "poly_book": PolymarketBook("will-btc-hit-100k"),
    "kalshi_book": KalshiBook("KXBTC-25FEB16"),
    "predictit": PredictItFeed(market_id=7456, contract_id=28562),
    "manifold": ManifoldFeed("will-btc-hit-100k-by-2026"),
    "espn": ESPNFeed("basketball", "nba"),
    "nws_forecast": NWSFeed(office="TOP", grid_x=31, grid_y=80),
    "nws_alerts": NWSFeed(state="FL", mode="alerts"),
    "json_path": RESTJsonPathFeed(
        url="https://api.coingecko.com/api/v3/simple/price",
        price_path="bitcoin.usd",
    ),
}

check("All 10 feed types instantiate", len(feeds) == 10)
for name, feed in feeds.items():
    check(f"Feed {name} has _type", hasattr(feed, '_type'), f"missing _type")


# ---------------------------------------------------------------------------
# Risk builder test
# ---------------------------------------------------------------------------
print("\n--- Risk builder ---")

risk = Risk(max_position=200.0, max_notional=5000.0, max_drawdown_pct=10.0,
            max_order_size=100.0, rate_limit=100, rate_burst=500)
cfg = risk.to_config()
check("Risk to_config returns RiskConfig", isinstance(cfg, RiskConfig))
check("Risk max_position_per_market", cfg.max_position_per_market == 200.0,
      f"got {cfg.max_position_per_market}")
check("Risk max_portfolio_notional", cfg.max_portfolio_notional == 5000.0,
      f"got {cfg.max_portfolio_notional}")
check("Risk max_daily_drawdown_pct", cfg.max_daily_drawdown_pct == 10.0,
      f"got {cfg.max_daily_drawdown_pct}")
check("Risk max_order_size", cfg.max_order_size == 100.0,
      f"got {cfg.max_order_size}")


print()
print("=" * 60)
print("SUMMARY")
print("=" * 60)
if errors:
    print(f"\n{len(errors)} ERRORS:")
    for e in errors:
        print(f"  {e}")
    sys.exit(1)
else:
    print("\nAll strategy.py tests PASSED")
