"""Tests for multi-outcome event support.

Covers: Outcome, Event, Market extensions, Engine event registry,
event risk limits, and backward compatibility.
"""

import pytest

from horizon._horizon import (
    Engine,
    Event,
    EventArbitrageOpportunity,
    Fill,
    Market,
    OrderRequest,
    OrderSide,
    Outcome,
    RiskConfig,
    Side,
)


# ---------------------------------------------------------------------------
# Outcome tests
# ---------------------------------------------------------------------------


class TestOutcome:
    def test_create(self):
        o = Outcome(name="Trump", market_id="trump-win")
        assert o.name == "Trump"
        assert o.market_id == "trump-win"
        assert o.yes_token_id is None
        assert o.no_token_id is None
        assert o.yes_price == 0.0

    def test_create_with_tokens(self):
        o = Outcome(
            name="Biden",
            market_id="biden-win",
            yes_token_id="t1",
            no_token_id="t2",
            yes_price=0.45,
        )
        assert o.yes_token_id == "t1"
        assert o.no_token_id == "t2"
        assert o.yes_price == 0.45

    def test_token_id_yes(self):
        o = Outcome(name="A", market_id="a", yes_token_id="y1", no_token_id="n1")
        assert o.token_id(Side.Yes) == "y1"

    def test_token_id_no(self):
        o = Outcome(name="A", market_id="a", yes_token_id="y1", no_token_id="n1")
        assert o.token_id(Side.No) == "n1"

    def test_token_id_none(self):
        o = Outcome(name="A", market_id="a")
        assert o.token_id(Side.Yes) is None
        assert o.token_id(Side.No) is None

    def test_repr(self):
        o = Outcome(name="Trump", market_id="trump-win", yes_price=0.55)
        r = repr(o)
        assert "Trump" in r
        assert "trump-win" in r


# ---------------------------------------------------------------------------
# Event tests
# ---------------------------------------------------------------------------


def _make_event():
    o1 = Outcome(name="Trump", market_id="trump-win", yes_token_id="t1", no_token_id="t2")
    o2 = Outcome(name="Biden", market_id="biden-win", yes_token_id="t3", no_token_id="t4")
    o3 = Outcome(name="DeSantis", market_id="desantis-win", yes_token_id="t5", no_token_id="t6")
    return Event(
        id="election",
        name="Who wins the 2024 election?",
        outcomes=[o1, o2, o3],
        neg_risk=True,
        exchange="polymarket",
        condition_id="cond_123",
    )


class TestEvent:
    def test_create(self):
        e = _make_event()
        assert e.id == "election"
        assert e.name == "Who wins the 2024 election?"
        assert e.neg_risk is True
        assert e.exchange == "polymarket"
        assert e.condition_id == "cond_123"

    def test_outcome_count(self):
        e = _make_event()
        assert e.outcome_count() == 3

    def test_outcome_names(self):
        e = _make_event()
        names = e.outcome_names()
        assert names == ["Trump", "Biden", "DeSantis"]

    def test_outcome_by_name_found(self):
        e = _make_event()
        o = e.outcome_by_name("Biden")
        assert o is not None
        assert o.market_id == "biden-win"

    def test_outcome_by_name_not_found(self):
        e = _make_event()
        o = e.outcome_by_name("Harris")
        assert o is None

    def test_to_markets(self):
        e = _make_event()
        markets = e.to_markets()
        assert len(markets) == 3

        m0 = markets[0]
        assert m0.id == "trump-win"
        assert m0.event_id == "election"
        assert m0.outcome_name == "Trump"
        assert m0.neg_risk is True
        assert m0.exchange == "polymarket"
        assert m0.yes_token_id == "t1"
        assert m0.no_token_id == "t2"
        assert m0.condition_id == "cond_123"

    def test_to_markets_names(self):
        e = _make_event()
        markets = e.to_markets()
        # Name should include event name + outcome name
        assert "Trump" in markets[0].name
        assert "Biden" in markets[1].name

    def test_repr(self):
        e = _make_event()
        r = repr(e)
        assert "election" in r
        assert "3" in r  # outcome count


# ---------------------------------------------------------------------------
# Market extension tests
# ---------------------------------------------------------------------------


class TestMarketExtensions:
    def test_event_id_default_none(self):
        m = Market(id="test")
        assert m.event_id is None
        assert m.outcome_name is None

    def test_event_id_set(self):
        m = Market(id="test", event_id="ev1", outcome_name="Trump")
        assert m.event_id == "ev1"
        assert m.outcome_name == "Trump"

    def test_backward_compat(self):
        """Existing Market creation without event fields still works."""
        m = Market(
            id="test",
            name="Test Market",
            slug="test",
            exchange="paper",
            yes_token_id="y1",
            no_token_id="n1",
        )
        assert m.id == "test"
        assert m.event_id is None
        assert m.outcome_name is None


# ---------------------------------------------------------------------------
# RiskConfig extension
# ---------------------------------------------------------------------------


class TestRiskConfigExtension:
    def test_default_none(self):
        rc = RiskConfig()
        assert rc.max_position_per_event is None

    def test_set_value(self):
        rc = RiskConfig(max_position_per_event=500.0)
        assert rc.max_position_per_event == 500.0


# ---------------------------------------------------------------------------
# Engine event registry tests
# ---------------------------------------------------------------------------


def _make_engine(**kwargs):
    return Engine(**kwargs)


class TestEngineEventRegistry:
    def test_register_event(self):
        engine = _make_engine()
        engine.register_event("election", ["trump-win", "biden-win", "desantis-win"])
        events = engine.registered_events()
        assert "election" in events
        assert len(events["election"]) == 3

    def test_event_exposure_empty(self):
        engine = _make_engine()
        engine.register_event("election", ["trump-win", "biden-win"])
        assert engine.event_exposure("election") == 0.0

    def test_event_exposure_with_fills(self):
        engine = _make_engine()
        engine.register_event("election", ["trump-win", "biden-win"])

        # Buy YES on both outcomes
        f1 = Fill(
            fill_id="f1", order_id="o1", market_id="trump-win",
            side=Side.Yes, order_side=OrderSide.Buy, price=0.55, size=10.0,
        )
        f2 = Fill(
            fill_id="f2", order_id="o2", market_id="biden-win",
            side=Side.Yes, order_side=OrderSide.Buy, price=0.40, size=5.0,
        )
        engine.process_fill(f1)
        engine.process_fill(f2)

        exposure = engine.event_exposure("election")
        assert abs(exposure - 15.0) < 1e-10

    def test_event_positions(self):
        engine = _make_engine()
        engine.register_event("election", ["trump-win", "biden-win"])

        f1 = Fill(
            fill_id="f1", order_id="o1", market_id="trump-win",
            side=Side.Yes, order_side=OrderSide.Buy, price=0.55, size=10.0,
        )
        engine.process_fill(f1)

        positions = engine.event_positions("election")
        assert len(positions) == 1
        assert positions[0].market_id == "trump-win"

    def test_market_event_id(self):
        engine = _make_engine()
        engine.register_event("election", ["trump-win", "biden-win"])
        assert engine.market_event_id("trump-win") == "election"
        assert engine.market_event_id("some-other-market") is None

    def test_event_exposure_unknown_event(self):
        engine = _make_engine()
        assert engine.event_exposure("nonexistent") == 0.0

    def test_event_positions_unknown_event(self):
        engine = _make_engine()
        assert engine.event_positions("nonexistent") == []


# ---------------------------------------------------------------------------
# Event risk limit tests
# ---------------------------------------------------------------------------


class TestEventRiskLimit:
    def test_event_risk_blocks_when_exceeded(self):
        rc = RiskConfig(max_position_per_event=20.0)
        engine = _make_engine(risk_config=rc)
        engine.register_event("election", ["trump-win", "biden-win"])

        # Fill 15 units on trump-win
        f1 = Fill(
            fill_id="f1", order_id="o1", market_id="trump-win",
            side=Side.Yes, order_side=OrderSide.Buy, price=0.55, size=15.0,
        )
        engine.process_fill(f1)

        # Try to buy 10 more on biden-win (total event exposure 15+10=25 > 20)
        req = OrderRequest(
            market_id="biden-win",
            side=Side.Yes,
            order_side=OrderSide.Buy,
            size=10.0,
            price=0.40,
        )
        with pytest.raises(RuntimeError, match="event position limit exceeded"):
            engine.submit_order(req)

    def test_event_risk_passes_under_limit(self):
        rc = RiskConfig(max_position_per_event=50.0)
        engine = _make_engine(risk_config=rc)
        engine.register_event("election", ["trump-win", "biden-win"])

        # Buy 10 on trump-win (total event exposure = 10 < 50)
        req = OrderRequest(
            market_id="trump-win",
            side=Side.Yes,
            order_side=OrderSide.Buy,
            size=10.0,
            price=0.55,
        )
        # Should not raise
        oid = engine.submit_order(req)
        assert oid  # got a valid order ID

    def test_binary_market_unaffected_by_event_risk(self):
        """Markets not registered in an event skip event risk check."""
        rc = RiskConfig(max_position_per_event=5.0)
        engine = _make_engine(risk_config=rc)
        # Don't register any event

        # Submit order on a standalone binary market
        req = OrderRequest(
            market_id="standalone-market",
            side=Side.Yes,
            order_side=OrderSide.Buy,
            size=10.0,
            price=0.55,
        )
        # Should pass - no event limit check
        oid = engine.submit_order(req)
        assert oid

    def test_sell_reduces_event_exposure(self):
        rc = RiskConfig(max_position_per_event=20.0)
        engine = _make_engine(risk_config=rc)
        engine.register_event("election", ["trump-win", "biden-win"])

        # Fill 18 units on trump-win
        f1 = Fill(
            fill_id="f1", order_id="o1", market_id="trump-win",
            side=Side.Yes, order_side=OrderSide.Buy, price=0.55, size=18.0,
        )
        engine.process_fill(f1)

        # Sell on trump-win should pass (reduces exposure)
        req = OrderRequest(
            market_id="trump-win",
            side=Side.Yes,
            order_side=OrderSide.Sell,
            size=5.0,
            price=0.60,
        )
        oid = engine.submit_order(req)
        assert oid


# ---------------------------------------------------------------------------
# Side x OrderSide matrix (all 4 combinations per outcome)
# ---------------------------------------------------------------------------


class TestSideOrderSideMatrix:
    """Test all 4 combinations: Buy YES, Sell YES, Buy NO, Sell NO."""

    def _engine_with_event(self):
        engine = _make_engine()
        engine.register_event("election", ["trump-win", "biden-win"])
        return engine

    def test_buy_yes(self):
        engine = self._engine_with_event()
        req = OrderRequest(
            market_id="trump-win",
            side=Side.Yes,
            order_side=OrderSide.Buy,
            size=5.0,
            price=0.55,
        )
        oid = engine.submit_order(req)
        assert oid

    def test_sell_yes(self):
        engine = self._engine_with_event()
        # First buy to have something to sell
        f = Fill(
            fill_id="f1", order_id="o1", market_id="trump-win",
            side=Side.Yes, order_side=OrderSide.Buy, price=0.55, size=10.0,
        )
        engine.process_fill(f)

        req = OrderRequest(
            market_id="trump-win",
            side=Side.Yes,
            order_side=OrderSide.Sell,
            size=5.0,
            price=0.60,
        )
        oid = engine.submit_order(req)
        assert oid

    def test_buy_no(self):
        engine = self._engine_with_event()
        req = OrderRequest(
            market_id="trump-win",
            side=Side.No,
            order_side=OrderSide.Buy,
            size=5.0,
            price=0.45,
        )
        oid = engine.submit_order(req)
        assert oid

    def test_sell_no(self):
        engine = self._engine_with_event()
        # First buy NO to have something to sell
        f = Fill(
            fill_id="f1", order_id="o1", market_id="trump-win",
            side=Side.No, order_side=OrderSide.Buy, price=0.45, size=10.0,
        )
        engine.process_fill(f)

        req = OrderRequest(
            market_id="trump-win",
            side=Side.No,
            order_side=OrderSide.Sell,
            size=5.0,
            price=0.50,
        )
        oid = engine.submit_order(req)
        assert oid


# ---------------------------------------------------------------------------
# Discovery tests (mock)
# ---------------------------------------------------------------------------


class TestDiscoverEvents:
    def test_extract_outcome_name(self):
        from horizon.discovery import _extract_outcome_name

        assert _extract_outcome_name(
            "Will Trump win the election?", "Who wins the election"
        ) != ""

        # Simple case: question IS the outcome
        assert _extract_outcome_name("Trump", "") == "Trump"

    def test_discover_events_non_polymarket(self):
        from horizon.discovery import discover_events

        events = discover_events(exchange="kalshi")
        assert events == []

    def test_discover_events_mock(self, monkeypatch):
        """Mock the Gamma API /events response."""
        from unittest.mock import MagicMock
        import horizon.discovery as disc

        mock_resp = MagicMock()
        mock_resp.raise_for_status = MagicMock()
        mock_resp.json.return_value = [
            {
                "slug": "election-2024",
                "title": "Who wins the 2024 election?",
                "neg_risk": True,
                "condition_id": "cond_abc",
                "markets": [
                    {
                        "slug": "trump-wins",
                        "question": "Will Trump win?",
                        "tokens": [
                            {"outcome": "Yes", "token_id": "tok_y1"},
                            {"outcome": "No", "token_id": "tok_n1"},
                        ],
                        "outcomePrices": "0.55,0.45",
                    },
                    {
                        "slug": "biden-wins",
                        "question": "Will Biden win?",
                        "tokens": [
                            {"outcome": "Yes", "token_id": "tok_y2"},
                            {"outcome": "No", "token_id": "tok_n2"},
                        ],
                        "outcomePrices": "0.30,0.70",
                    },
                ],
            }
        ]

        import requests
        monkeypatch.setattr(requests, "get", lambda *a, **kw: mock_resp)

        events = disc.discover_events(exchange="polymarket", limit=10)
        assert len(events) == 1

        e = events[0]
        assert e.id == "election-2024"
        assert e.outcome_count() == 2
        assert e.neg_risk is True

        names = e.outcome_names()
        assert len(names) == 2

        # Check outcomes have token IDs
        o0 = e.outcomes[0]
        assert o0.yes_token_id == "tok_y1"
        assert o0.no_token_id == "tok_n1"


# ---------------------------------------------------------------------------
# Context extensions
# ---------------------------------------------------------------------------


class TestContextEventExtensions:
    def test_context_event_field(self):
        from horizon.context import Context
        e = _make_event()
        ctx = Context(event=e)
        assert ctx.event is not None
        assert ctx.event.id == "election"

    def test_context_event_default_none(self):
        from horizon.context import Context
        ctx = Context()
        assert ctx.event is None

    def test_inventory_net_for_event(self):
        from horizon.context import InventorySnapshot
        from horizon._horizon import Position

        # Build positions by processing fills through engine
        engine = _make_engine()
        f1 = Fill(
            fill_id="f1", order_id="o1", market_id="trump-win",
            side=Side.Yes, order_side=OrderSide.Buy, price=0.55, size=10.0,
        )
        f2 = Fill(
            fill_id="f2", order_id="o2", market_id="biden-win",
            side=Side.No, order_side=OrderSide.Buy, price=0.40, size=5.0,
        )
        engine.process_fill(f1)
        engine.process_fill(f2)

        inv = InventorySnapshot(positions=engine.positions())
        net = inv.net_for_event(["trump-win", "biden-win"])
        # YES 10 - NO 5 = 5
        assert abs(net - 5.0) < 1e-10

    def test_inventory_positions_for_event(self):
        from horizon.context import InventorySnapshot

        engine = _make_engine()
        f1 = Fill(
            fill_id="f1", order_id="o1", market_id="trump-win",
            side=Side.Yes, order_side=OrderSide.Buy, price=0.55, size=10.0,
        )
        f2 = Fill(
            fill_id="f2", order_id="o2", market_id="other-market",
            side=Side.Yes, order_side=OrderSide.Buy, price=0.60, size=5.0,
        )
        engine.process_fill(f1)
        engine.process_fill(f2)

        inv = InventorySnapshot(positions=engine.positions())
        event_pos = inv.positions_for_event(["trump-win"])
        assert len(event_pos) == 1
        assert event_pos[0].market_id == "trump-win"
