"""Phase 4: Exchange & Market Audit - Paper Exchange, Market Discovery, Security.

Tests paper exchange behavior, public market discovery APIs, multi-exchange
routing, and URL injection prevention.
"""

import pytest

from horizon._horizon import (
    Engine,
    Fill,
    Market,
    OrderRequest,
    OrderSide,
    OrderType,
    RiskConfig,
    Side,
    TimeInForce,
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def make_engine(**kw):
    defaults = dict(
        risk_config=RiskConfig(
            max_position_per_market=100.0,
            max_portfolio_notional=10000.0,
            max_order_size=100.0,
        ),
    )
    defaults.update(kw)
    return Engine(**defaults)


def buy(market="mkt_1", price=0.55, size=10.0, **kw):
    return OrderRequest(
        market_id=market,
        side=Side.Yes,
        order_side=OrderSide.Buy,
        size=size,
        price=price,
        order_type=kw.get("order_type", OrderType.Limit),
        time_in_force=kw.get("time_in_force", TimeInForce.GTC),
    )


def sell(market="mkt_1", price=0.60, size=10.0, **kw):
    return OrderRequest(
        market_id=market,
        side=Side.Yes,
        order_side=OrderSide.Sell,
        size=size,
        price=price,
        order_type=kw.get("order_type", OrderType.Limit),
        time_in_force=kw.get("time_in_force", TimeInForce.GTC),
    )


# ===================================================================
# 4a. Paper Exchange
# ===================================================================

class TestPaperExchange:
    """Paper exchange order ID format, matching, and cancellation."""

    def test_order_id_starts_with_p(self):
        """Paper order IDs should start with 'p'."""
        e = make_engine()
        oid = e.submit_order(buy())
        assert oid.startswith("p")

    def test_order_ids_sequential(self):
        """Paper order IDs should be sequentially numbered."""
        e = make_engine()
        ids = []
        for i in range(5):
            ids.append(e.submit_order(buy(market=f"m{i}", price=0.50, size=1.0)))

        nums = [int(oid[1:]) for oid in ids]
        for i in range(1, len(nums)):
            assert nums[i] == nums[i - 1] + 1

    def test_cancel_all_clears_orders(self):
        """cancel_all() should remove all open orders."""
        e = make_engine()
        for i in range(10):
            e.submit_order(buy(market=f"m{i}", price=0.50, size=1.0))

        assert e.status().open_orders == 10
        count = e.cancel_all()
        assert count == 10
        assert e.status().open_orders == 0

    def test_limit_buy_fills_when_price_drops(self):
        """Buy limit at 0.55 fills when mid drops to 0.50."""
        e = make_engine()
        e.submit_order(buy(price=0.55, size=10.0))
        fills = e.tick("mkt_1", 0.50)
        assert fills == 1

    def test_limit_buy_does_not_fill_above(self):
        """Buy limit at 0.55 does NOT fill when mid is 0.60."""
        e = make_engine()
        e.submit_order(buy(price=0.55, size=10.0))
        fills = e.tick("mkt_1", 0.60)
        assert fills == 0

    def test_limit_sell_fills_when_price_rises(self):
        """Sell limit at 0.60 fills when mid rises to 0.65."""
        e = make_engine()
        e.submit_order(sell(price=0.60, size=10.0))
        fills = e.tick("mkt_1", 0.65)
        assert fills == 1

    def test_limit_sell_does_not_fill_below(self):
        """Sell limit at 0.60 does NOT fill when mid is 0.55."""
        e = make_engine()
        e.submit_order(sell(price=0.60, size=10.0))
        fills = e.tick("mkt_1", 0.55)
        assert fills == 0

    def test_market_order_always_fills(self):
        """Market orders fill on any tick regardless of price."""
        e = make_engine()
        e.submit_order(buy(price=0.0, size=5.0, order_type=OrderType.Market))
        fills = e.tick("mkt_1", 0.75)
        assert fills == 1

    def test_market_order_canceled_if_not_ticked(self):
        """Market orders should be canceled after tick if unfilled."""
        e = make_engine()
        e.submit_order(buy(price=0.0, size=5.0, order_type=OrderType.Market))
        # Market orders always match on tick, so they fill:
        e.tick("mkt_1", 0.50)
        # After fill, no open orders
        assert e.status().open_orders == 0

    def test_cancel_single_order(self):
        """cancel(order_id) should cancel one specific order."""
        e = make_engine()
        oid1 = e.submit_order(buy(market="m1", price=0.50, size=1.0))
        oid2 = e.submit_order(buy(market="m2", price=0.51, size=1.0))

        e.cancel(oid1)
        assert e.status().open_orders == 1

    def test_partial_fill_ratio_clamped(self):
        """partial_fill_ratio must be in (0, 1]."""
        # Exactly 1.0 should work
        e = make_engine(paper_partial_fill_ratio=1.0)
        assert e is not None

        # 0.0 or less should fail
        with pytest.raises((ValueError, RuntimeError)):
            Engine(paper_partial_fill_ratio=0.0)


# ===================================================================
# 4b. Market Discovery (public APIs, no auth)
# ===================================================================

class TestMarketDiscovery:
    """Public market discovery - no API keys needed."""

    def test_polymarket_discover(self):
        """discover_markets for Polymarket returns results."""
        try:
            from horizon._horizon import discover_markets
            results = discover_markets(exchange="polymarket", limit=3)
            assert isinstance(results, list)
            if len(results) > 0:
                assert hasattr(results[0], "id") or "id" in results[0]
        except ImportError:
            pytest.skip("discover_markets not available")
        except Exception as exc:
            if "network" in str(exc).lower() or "connection" in str(exc).lower():
                pytest.skip(f"Network unavailable: {exc}")
            raise

    def test_kalshi_discover(self):
        """discover_markets for Kalshi returns results."""
        try:
            from horizon._horizon import discover_markets
            results = discover_markets(exchange="kalshi", limit=3)
            assert isinstance(results, list)
        except ImportError:
            pytest.skip("discover_markets not available")
        except Exception as exc:
            if "network" in str(exc).lower() or "connection" in str(exc).lower():
                pytest.skip(f"Network unavailable: {exc}")
            raise


# ===================================================================
# 4c. Multi-Exchange Engine
# ===================================================================

class TestMultiExchange:
    """Multi-exchange engine configuration and routing."""

    def test_add_second_exchange_rejects_duplicate(self):
        """Adding a duplicate exchange type raises ValueError."""
        e = make_engine()
        with pytest.raises(ValueError, match="already registered"):
            e.add_exchange("paper")

    def test_submit_to_named_exchange(self):
        """Submit order specifying target exchange by name."""
        e = make_engine()
        # Default paper exchange
        oid = e.submit_order(buy(), exchange="paper")
        assert oid.startswith("p")

    def test_submit_to_default(self):
        """Submit without exchange name routes to primary."""
        e = make_engine()
        oid = e.submit_order(buy())
        assert oid.startswith("p")


# ===================================================================
# 4d. URL Injection Prevention
# ===================================================================

class TestURLInjection:
    """Verify URL parameter validation prevents injection attacks."""

    def test_kalshi_validate_url_param(self):
        """Kalshi should reject path traversal in URL params."""
        try:
            from horizon._horizon import validate_url_param
            with pytest.raises((ValueError, RuntimeError)):
                validate_url_param("../etc/passwd")
        except ImportError:
            # validate_url_param is internal - test via order cancel instead
            e = Engine(
                exchange_type="kalshi",
                risk_config=RiskConfig(),
                email="test@test.com",
                password="test123",
            )
            # Attempting to cancel with a path traversal ID should fail
            # (though it may fail for other reasons in paper mode)
            pytest.skip("validate_url_param not exposed to Python")

    def test_market_id_special_chars(self):
        """Market IDs with special characters should be handled safely."""
        e = make_engine()
        # Should not crash or cause injection - just treated as a string
        oid = e.submit_order(buy(market="mkt<script>alert(1)</script>"))
        assert oid.startswith("p")

    def test_order_request_with_empty_market_id(self):
        """Empty market ID in order request should be handled."""
        e = make_engine()
        # May reject or accept - should not crash
        try:
            e.submit_order(buy(market=""))
        except (ValueError, RuntimeError):
            pass  # Rejection is fine

    def test_very_long_market_id(self):
        """Very long market ID should not cause buffer overflow."""
        e = make_engine()
        long_id = "x" * 10000
        try:
            e.submit_order(buy(market=long_id))
        except (ValueError, RuntimeError):
            pass  # Rejection is fine - no crash is what matters


# ===================================================================
# 4e. Market Registration
# ===================================================================

class TestMarketRegistration:
    """Market lifecycle management."""

    def test_register_event(self):
        """Events can be registered with the engine."""
        e = make_engine()
        e.register_event("evt_1", ["mkt_A", "mkt_B"])
        # Should not crash

    def test_registered_market_attributes(self):
        """Registered market should preserve attributes."""
        m = Market(
            "mkt_123",
            name="Presidential Election",
            exchange="polymarket",
            event_id="evt_456",
        )
        assert m.id == "mkt_123"
        assert m.name == "Presidential Election"
        assert m.exchange == "polymarket"
        assert m.event_id == "evt_456"

    def test_market_default_values(self):
        """Market constructor defaults should be sensible."""
        m = Market("simple_mkt")
        assert m.name == ""
        assert m.exchange == "paper"
        assert m.active is True
        assert m.neg_risk is False
        assert m.yes_token_id is None
        assert m.no_token_id is None
