"""Phase 8: End-to-End Integration - Pipeline, Multi-Market, Performance.

Tests the full pipeline from strategy composition through order execution,
including multi-market routing, performance benchmarks, and export verification.
"""

import time

import pytest

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


# ---------------------------------------------------------------------------
# E2E Pipeline Tests
# ---------------------------------------------------------------------------

class TestPipelineE2E:
    """Full pipeline composition and execution."""

    def test_single_function_returns_fair_value(self):
        """Single pipeline function returns a fair value."""
        def fair_value(ctx: Context) -> float:
            return ctx.feed.price * 0.95

        ctx = Context(feeds={"default": FeedData(price=0.60)})
        result = _run_pipeline([fair_value], ctx)
        assert abs(result - 0.57) < 1e-10

    def test_two_stage_pipeline_returns_quotes(self):
        """Two-stage: fair_value → quoter returns Quote list."""
        def fair_value(ctx: Context) -> float:
            return 0.50

        def quoter(ctx: Context, fair: float) -> list[Quote]:
            return [Quote(bid=fair - 0.03, ask=fair + 0.03, size=10.0)]

        ctx = Context(feeds={"default": FeedData(price=0.50)})
        result = _run_pipeline([fair_value, quoter], ctx)
        assert len(result) == 1
        assert result[0].bid == pytest.approx(0.47)
        assert result[0].ask == pytest.approx(0.53)

    def test_three_stage_pipeline(self):
        """Three-stage: fair_value → spread_adjust → quoter."""
        def fair_value(ctx: Context) -> float:
            return 0.55

        def spread_adjust(ctx: Context) -> float:
            return 0.04  # wider spread

        def quoter(ctx: Context, fair: float, spread: float) -> list[Quote]:
            half = spread / 2
            return [Quote(bid=fair - half, ask=fair + half, size=5.0)]

        ctx = Context()
        result = _run_pipeline([fair_value, spread_adjust, quoter], ctx)
        assert len(result) == 1
        assert result[0].bid == pytest.approx(0.53)
        assert result[0].ask == pytest.approx(0.57)

    def test_pipeline_with_inventory_context(self):
        """Pipeline can access inventory from context."""
        def fair_value(ctx: Context) -> float:
            # Adjust for inventory
            return 0.50 - ctx.inventory.net * 0.001

        engine = Engine(risk_config=RiskConfig())
        ctx = _build_context(engine, Market(id="mkt_1"), {}, {})
        result = _run_pipeline([fair_value], ctx)
        assert abs(result - 0.50) < 1e-10  # No inventory, no adjustment

    def test_pipeline_with_params(self):
        """Pipeline can access custom params from context."""
        def fair_value(ctx: Context) -> float:
            gamma = ctx.params.get("gamma", 0.1)
            return 0.50 * (1 - gamma)

        engine = Engine(risk_config=RiskConfig())
        ctx = _build_context(engine, Market(id="mkt_1"), {}, {"gamma": 0.2})
        result = _run_pipeline([fair_value], ctx)
        assert abs(result - 0.40) < 1e-10

    def test_context_feed_staleness_integration(self):
        """Context feed data staleness check works in pipeline."""
        def safe_fair(ctx: Context) -> float | None:
            if ctx.feed.is_stale():
                return None
            return ctx.feed.price

        # Stale feed
        ctx = Context(feeds={"default": FeedData(price=0.50, timestamp=0.0)})
        result = _run_pipeline([safe_fair], ctx)
        assert result is None

        # Fresh feed
        ctx2 = Context(feeds={"default": FeedData(price=0.50, timestamp=time.time())})
        result2 = _run_pipeline([safe_fair], ctx2)
        assert result2 == pytest.approx(0.50)


# ---------------------------------------------------------------------------
# Multi-Market / Multi-Feed Integration
# ---------------------------------------------------------------------------

class TestMultiMarketIntegration:
    """Multi-market, multi-feed wiring correctness."""

    def test_two_markets_independent_pipelines(self):
        """Dict pipeline routes to correct market."""
        def fair_a(ctx: Context) -> float:
            return 0.40

        def fair_b(ctx: Context) -> float:
            return 0.60

        pipeline = {
            "mkt_A": [fair_a],
            "mkt_B": [fair_b],
        }

        ctx_a = Context()
        ctx_b = Context()
        result_a = _run_pipeline(pipeline["mkt_A"], ctx_a)
        result_b = _run_pipeline(pipeline["mkt_B"], ctx_b)

        assert result_a == pytest.approx(0.40)
        assert result_b == pytest.approx(0.60)

    def test_multi_feed_context(self):
        """Context with multiple feeds can access each by name."""
        feeds = {
            "btc": FeedData(price=50000.0, timestamp=time.time()),
            "eth": FeedData(price=3000.0, timestamp=time.time()),
        }
        ctx = Context(feeds=feeds)
        assert ctx.feeds["btc"].price == pytest.approx(50000.0)
        assert ctx.feeds["eth"].price == pytest.approx(3000.0)

    def test_default_feed_property(self):
        """context.feed returns 'default' feed or first available."""
        # With 'default' key
        ctx1 = Context(feeds={"default": FeedData(price=0.55)})
        assert ctx1.feed.price == pytest.approx(0.55)

        # Without 'default', returns first
        ctx2 = Context(feeds={"btc": FeedData(price=100.0)})
        assert ctx2.feed.price == pytest.approx(100.0)

        # Empty feeds returns empty FeedData
        ctx3 = Context()
        assert ctx3.feed.price == 0.0


# ---------------------------------------------------------------------------
# Paper Trading Full Cycle
# ---------------------------------------------------------------------------

class TestPaperTradingCycle:
    """End-to-end paper trading: submit quotes → tick → verify fills → PnL."""

    def test_full_mm_cycle(self):
        """Market making cycle: quote → fill buy → quote → fill sell → profit."""
        e = Engine(risk_config=RiskConfig(
            max_position_per_market=100.0,
            max_order_size=100.0,
        ))

        # Submit buy and sell quotes
        buy_id = e.submit_order(OrderRequest(
            market_id="mkt_1", side=Side.Yes, order_side=OrderSide.Buy,
            size=10.0, price=0.48,
        ))
        sell_id = e.submit_order(OrderRequest(
            market_id="mkt_1", side=Side.Yes, order_side=OrderSide.Sell,
            size=10.0, price=0.52,
        ))

        # Price drops → buy fills
        e.tick("mkt_1", 0.45)
        pos = e.positions()
        assert len(pos) == 1
        assert pos[0].size == pytest.approx(10.0)
        assert pos[0].avg_entry_price == pytest.approx(0.48)

        # Submit new sell (different price to avoid dedup)
        e.submit_order(OrderRequest(
            market_id="mkt_1", side=Side.Yes, order_side=OrderSide.Sell,
            size=10.0, price=0.53,
        ))

        # Price rises → sell fills
        e.tick("mkt_1", 0.55)

        # Position closed, realized PnL should be positive
        status = e.status()
        assert status.active_positions == 0
        assert status.total_realized_pnl > 0  # Bought at 0.48, sold at 0.53

    def test_multi_market_paper_cycle(self):
        """Multiple markets trading simultaneously."""
        e = Engine(risk_config=RiskConfig(
            max_position_per_market=100.0,
            max_portfolio_notional=10000.0,
            max_order_size=100.0,
        ))

        # Submit orders for 3 markets
        for i in range(3):
            e.submit_order(OrderRequest(
                market_id=f"mkt_{i}", side=Side.Yes, order_side=OrderSide.Buy,
                size=10.0, price=0.50,
            ))

        # Tick all markets
        for i in range(3):
            e.tick(f"mkt_{i}", 0.45)

        assert e.status().active_positions == 3
        assert e.status().open_orders == 0


# ---------------------------------------------------------------------------
# Performance Benchmarks
# ---------------------------------------------------------------------------

class TestPerformanceBenchmarks:
    """Latency and throughput benchmarks for hot paths."""

    def test_submit_order_latency(self):
        """1000 submit_order calls - measure throughput."""
        e = Engine(risk_config=RiskConfig(
            max_position_per_market=100000.0,
            max_portfolio_notional=1000000.0,
            max_order_size=100000.0,
            rate_limit_burst=10000,
            rate_limit_sustained=5000,
            dedup_window_ms=0,  # Disable dedup for benchmark
        ))

        start = time.monotonic()
        for i in range(1000):
            e.submit_order(OrderRequest(
                market_id=f"mkt_{i}",
                side=Side.Yes,
                order_side=OrderSide.Buy,
                size=1.0,
                price=0.50,
            ))
        elapsed = time.monotonic() - start

        # Target: < 100ms for 1000 orders (< 100us each)
        assert elapsed < 1.0, f"1000 orders took {elapsed:.3f}s (target < 1.0s)"
        ops_per_sec = 1000 / elapsed
        print(f"\nSubmit throughput: {ops_per_sec:.0f} orders/sec ({elapsed*1000:.1f}ms total)")

    def test_tick_latency(self):
        """1000 tick calls with orders - measure throughput."""
        e = Engine(risk_config=RiskConfig(
            max_position_per_market=100000.0,
            max_portfolio_notional=1000000.0,
            max_order_size=100000.0,
            rate_limit_burst=10000,
            rate_limit_sustained=5000,
            dedup_window_ms=0,
        ))

        # Submit 100 orders across 100 markets
        for i in range(100):
            e.submit_order(OrderRequest(
                market_id=f"mkt_{i}",
                side=Side.Yes,
                order_side=OrderSide.Buy,
                size=1.0,
                price=0.50,
            ))

        start = time.monotonic()
        for i in range(1000):
            e.tick(f"mkt_{i % 100}", 0.45)
        elapsed = time.monotonic() - start

        assert elapsed < 1.0, f"1000 ticks took {elapsed:.3f}s"
        print(f"\nTick throughput: {1000/elapsed:.0f} ticks/sec ({elapsed*1000:.1f}ms total)")

    def test_fill_processing_throughput(self):
        """1000 fills processed - measure throughput."""
        e = Engine(risk_config=RiskConfig(
            max_position_per_market=100000.0,
            max_portfolio_notional=1000000.0,
            max_order_size=100000.0,
        ))

        start = time.monotonic()
        for i in range(1000):
            e.process_fill(Fill(
                fill_id=f"f{i}",
                order_id=f"o{i}",
                market_id=f"mkt_{i % 50}",
                side=Side.Yes,
                order_side=OrderSide.Buy,
                price=0.50,
                size=1.0,
            ))
        elapsed = time.monotonic() - start

        assert elapsed < 1.0, f"1000 fills took {elapsed:.3f}s"
        print(f"\nFill throughput: {1000/elapsed:.0f} fills/sec ({elapsed*1000:.1f}ms total)")


# ---------------------------------------------------------------------------
# Export Verification
# ---------------------------------------------------------------------------

class TestExportVerification:
    """Verify all public API exports are importable."""

    def test_all_exports_importable(self):
        """Every name in horizon.__all__ should be importable."""
        import horizon
        failed = []
        for name in horizon.__all__:
            obj = getattr(horizon, name, None)
            if obj is None:
                failed.append(name)

        assert len(failed) == 0, f"Failed to import: {failed[:20]}"

    def test_core_type_constructors(self):
        """Core Rust types should be constructible from Python."""
        from horizon._horizon import (
            Engine, Market, OrderRequest, RiskConfig, Quote,
            Fill, Side, OrderSide, OrderType, TimeInForce,
        )

        # Each should construct without error
        m = Market("test")
        q = Quote(bid=0.45, ask=0.55, size=10.0)
        r = RiskConfig()
        f = Fill(fill_id="f1", order_id="o1", market_id="m1",
                 side=Side.Yes, order_side=OrderSide.Buy,
                 price=0.50, size=1.0)
        o = OrderRequest(market_id="m1", side=Side.Yes,
                         order_side=OrderSide.Buy, size=1.0, price=0.50)
        e = Engine()

        assert m.id == "test"
        assert q.bid == pytest.approx(0.45)
        assert f.fill_id == "f1"

    def test_cli_help(self):
        """CLI --help should work."""
        import subprocess
        result = subprocess.run(
            ["python", "-m", "horizon", "--help"],
            capture_output=True, text=True, timeout=10,
        )
        assert result.returncode == 0
        assert "run" in result.stdout
        assert "fills" in result.stdout
