"""Tests for market_type filter in discover() and market_detail()."""

import json
from unittest.mock import patch, MagicMock

import pytest

from horizon import tools
from horizon.discovery import get_market_detail


# ---------------------------------------------------------------------------
# Mock data
# ---------------------------------------------------------------------------

def _mock_polymarket_markets():
    return [
        {
            "slug": "will-btc-hit-100k",
            "question": "Will BTC hit $100k?",
            "tokens": [
                {"outcome": "Yes", "token_id": "tok_yes_1"},
                {"outcome": "No", "token_id": "tok_no_1"},
            ],
            "condition_id": "cond_1",
            "neg_risk": False,
            "volume": 50000,
            "active": True,
            "end_date_iso": "2030-12-31T00:00:00Z",
        },
    ]


def _mock_polymarket_events():
    return [
        {
            "slug": "presidential-election-2028",
            "title": "Who will win the 2028 election?",
            "condition_id": "cond_ev1",
            "neg_risk": True,
            "end_date_iso": "2028-11-05T00:00:00Z",
            "markets": [
                {
                    "slug": "trump-wins",
                    "question": "Will Trump win?",
                    "tokens": [
                        {"outcome": "Yes", "token_id": "t1"},
                        {"outcome": "No", "token_id": "t2"},
                    ],
                    "outcomePrices": '["0.55","0.45"]',
                    "volume": 100,
                },
                {
                    "slug": "harris-wins",
                    "question": "Will Harris win?",
                    "tokens": [
                        {"outcome": "Yes", "token_id": "t3"},
                        {"outcome": "No", "token_id": "t4"},
                    ],
                    "outcomePrices": '["0.35","0.65"]',
                    "volume": 100,
                },
            ],
        },
    ]


def _mock_market_detail_response():
    return [
        {
            "slug": "will-btc-hit-100k",
            "question": "Will BTC hit $100k?",
            "description": "Resolves YES if BTC hits 100k.",
            "createdAt": "2024-01-01T00:00:00Z",
            "end_date_iso": "2030-12-31T00:00:00Z",
            "volume": 50000,
            "liquidityNum": 12000,
            "image": "https://example.com/btc.png",
            "condition_id": "cond_1",
            "active": True,
            "outcomePrices": '["0.65","0.35"]',
            "tokens": [
                {"outcome": "Yes", "token_id": "tok_yes_1"},
                {"outcome": "No", "token_id": "tok_no_1"},
            ],
        },
    ]


# ---------------------------------------------------------------------------
# Market type filter tests
# ---------------------------------------------------------------------------


class TestDiscoverMarketType:
    @patch("horizon.discovery.requests.get")
    def test_binary_only(self, mock_get):
        mock_resp = MagicMock()
        mock_resp.raise_for_status = MagicMock()
        mock_resp.json.return_value = _mock_polymarket_markets()
        mock_get.return_value = mock_resp

        result = tools.discover("polymarket", "", 10, "binary")
        assert isinstance(result, list)
        # Should not have "type": "multi" items
        for item in result:
            assert item.get("type") != "multi"

    @patch("horizon.discovery.requests.get")
    def test_multi_only(self, mock_get):
        mock_resp = MagicMock()
        mock_resp.raise_for_status = MagicMock()
        mock_resp.json.return_value = _mock_polymarket_events()
        mock_get.return_value = mock_resp

        result = tools.discover("polymarket", "", 10, "multi")
        assert isinstance(result, list)
        for item in result:
            assert item.get("type") == "multi"

    @patch("horizon.discovery.requests.get")
    def test_all_returns_both(self, mock_get):
        """When market_type='all', both binary and multi results are returned."""
        call_count = [0]

        def side_effect(*args, **kwargs):
            call_count[0] += 1
            mock_resp = MagicMock()
            mock_resp.raise_for_status = MagicMock()
            url = args[0] if args else kwargs.get("url", "")
            if "/events" in url:
                mock_resp.json.return_value = _mock_polymarket_events()
            else:
                mock_resp.json.return_value = _mock_polymarket_markets()
            return mock_resp

        mock_get.side_effect = side_effect

        result = tools.discover("polymarket", "", 20, "all")
        assert isinstance(result, list)
        # Should have called both /markets and /events
        assert call_count[0] >= 2

    @patch("horizon.discovery.requests.get")
    def test_multi_not_available_on_kalshi(self, mock_get):
        """Kalshi has no multi-outcome events, so multi filter returns empty."""
        mock_resp = MagicMock()
        mock_resp.raise_for_status = MagicMock()
        mock_resp.json.return_value = {"markets": []}
        mock_get.return_value = mock_resp

        result = tools.discover("kalshi", "", 10, "multi")
        assert isinstance(result, list)
        assert len(result) == 0

    def test_default_market_type_is_all(self):
        """Default market_type should be 'all'."""
        import inspect
        sig = inspect.signature(tools.discover)
        assert sig.parameters["market_type"].default == "all"


# ---------------------------------------------------------------------------
# Market detail tests (Polymarket)
# ---------------------------------------------------------------------------


class TestMarketDetailPolymarket:
    @patch("horizon.discovery.requests.get")
    def test_basic_metadata(self, mock_get):
        mock_resp = MagicMock()
        mock_resp.raise_for_status = MagicMock()
        mock_resp.json.return_value = _mock_market_detail_response()
        mock_get.return_value = mock_resp

        result = get_market_detail("will-btc-hit-100k", exchange="polymarket")
        assert result["market"]["name"] == "Will BTC hit $100k?"
        assert result["market"]["slug"] == "will-btc-hit-100k"
        assert result["market"]["exchange"] == "polymarket"
        assert result["market"]["volume"] == 50000.0
        assert result["market"]["active"] is True
        assert result["market"]["condition_id"] == "cond_1"

    @patch("horizon.discovery.requests.get")
    def test_prices_extracted(self, mock_get):
        mock_resp = MagicMock()
        mock_resp.raise_for_status = MagicMock()
        mock_resp.json.return_value = _mock_market_detail_response()
        mock_get.return_value = mock_resp

        result = get_market_detail("will-btc-hit-100k")
        assert result["prices"]["yes_price"] == 0.65
        assert result["prices"]["no_price"] == 0.35

    @patch("horizon.discovery.requests.get")
    def test_not_found(self, mock_get):
        mock_resp = MagicMock()
        mock_resp.raise_for_status = MagicMock()
        mock_resp.json.return_value = []
        mock_get.return_value = mock_resp

        with pytest.raises(ValueError, match="Market not found"):
            get_market_detail("nonexistent-market")

    @patch("horizon.discovery.requests.get")
    def test_price_history_section_present(self, mock_get):
        mock_resp = MagicMock()
        mock_resp.raise_for_status = MagicMock()
        mock_resp.json.return_value = _mock_market_detail_response()
        mock_get.return_value = mock_resp

        # Patch the Rust function so it doesn't actually call the API
        with patch("horizon._horizon.fetch_polymarket_prices", side_effect=Exception("mock")):
            result = get_market_detail("will-btc-hit-100k")
            # On error, should still have the key with empty list
            assert "price_history" in result
            assert result["price_history"] == []

    @patch("horizon.discovery.requests.get")
    def test_orderbook_section_present(self, mock_get):
        mock_resp = MagicMock()
        mock_resp.raise_for_status = MagicMock()
        mock_resp.json.return_value = _mock_market_detail_response()
        mock_get.return_value = mock_resp

        with patch("horizon._horizon.fetch_polymarket_book", side_effect=Exception("mock")):
            result = get_market_detail("will-btc-hit-100k")
            assert "orderbook" in result
            assert result["orderbook"]["bids"] == []

    @patch("horizon.discovery.requests.get")
    def test_trades_section_present(self, mock_get):
        mock_resp = MagicMock()
        mock_resp.raise_for_status = MagicMock()
        mock_resp.json.return_value = _mock_market_detail_response()
        mock_get.return_value = mock_resp

        result = get_market_detail("will-btc-hit-100k")
        assert "recent_trades" in result

    @patch("horizon.discovery.requests.get")
    def test_holders_section_present(self, mock_get):
        mock_resp = MagicMock()
        mock_resp.raise_for_status = MagicMock()
        mock_resp.json.return_value = _mock_market_detail_response()
        mock_get.return_value = mock_resp

        result = get_market_detail("will-btc-hit-100k")
        assert "top_holders" in result

    @patch("horizon.discovery.requests.get")
    def test_all_sections_present(self, mock_get):
        mock_resp = MagicMock()
        mock_resp.raise_for_status = MagicMock()
        mock_resp.json.return_value = _mock_market_detail_response()
        mock_get.return_value = mock_resp

        result = get_market_detail("will-btc-hit-100k")
        expected_keys = {"market", "prices", "price_history", "orderbook", "recent_trades", "top_holders"}
        assert set(result.keys()) == expected_keys


# ---------------------------------------------------------------------------
# Market detail tests (Kalshi)
# ---------------------------------------------------------------------------


class TestMarketDetailKalshi:
    @patch("horizon._horizon.fetch_kalshi_market")
    @patch("horizon._horizon.fetch_kalshi_book")
    def test_kalshi_metadata(self, mock_book, mock_market):
        mock_market.return_value = json.dumps({
            "ticker": "KXBTC-25FEB28",
            "title": "Bitcoin above $100k?",
            "subtitle": "Will BTC be above 100k by Feb 28?",
            "status": "open",
            "expiration_time": "2025-02-28T00:00:00Z",
            "open_time": "2025-01-01T00:00:00Z",
            "volume": 5000,
            "yes_bid": 55,
            "no_bid": 45,
        })

        # Mock the orderbook - bids(), asks(), spread() are methods
        mock_snapshot = MagicMock()
        mock_snapshot.bids.return_value = [(0.55, 100.0), (0.50, 200.0)]
        mock_snapshot.asks.return_value = [(0.60, 150.0)]
        mock_snapshot.spread.return_value = 0.05
        mock_book.return_value = mock_snapshot

        result = get_market_detail("KXBTC-25FEB28", exchange="kalshi")
        assert result["market"]["name"] == "Bitcoin above $100k?"
        assert result["market"]["exchange"] == "kalshi"
        assert result["market"]["active"] is True
        assert result["prices"]["yes_price"] == 0.55
        assert result["orderbook"]["bids"][0]["price"] == 0.55

    @patch("horizon._horizon.fetch_kalshi_market")
    def test_kalshi_not_found(self, mock_market):
        mock_market.side_effect = RuntimeError("HTTP 404")

        with pytest.raises(RuntimeError, match="Failed to fetch Kalshi"):
            get_market_detail("NONEXISTENT", exchange="kalshi")


# ---------------------------------------------------------------------------
# Market detail via tools wrapper
# ---------------------------------------------------------------------------


class TestMarketDetailToolsWrapper:
    @patch("horizon.discovery.requests.get")
    def test_tools_wrapper_returns_dict(self, mock_get):
        mock_resp = MagicMock()
        mock_resp.raise_for_status = MagicMock()
        mock_resp.json.return_value = _mock_market_detail_response()
        mock_get.return_value = mock_resp

        result = tools.market_detail("will-btc-hit-100k")
        assert isinstance(result, dict)
        assert "market" in result

    @patch("horizon.discovery.get_market_detail")
    def test_tools_wrapper_error_handling(self, mock_detail):
        mock_detail.side_effect = ValueError("Market not found: foo")

        result = tools.market_detail("foo")
        assert isinstance(result, dict)
        assert "error" in result
        assert "Market not found" in result["error"]

    def test_invalid_exchange(self):
        result = tools.market_detail("some-market", exchange="invalid")
        assert isinstance(result, dict)
        assert "error" in result


# ---------------------------------------------------------------------------
# Edge cases
# ---------------------------------------------------------------------------


class TestEdgeCases:
    @patch("horizon.discovery.requests.get")
    def test_missing_outcome_prices(self, mock_get):
        """Market with no outcomePrices should default to 0."""
        data = _mock_market_detail_response()
        del data[0]["outcomePrices"]
        mock_resp = MagicMock()
        mock_resp.raise_for_status = MagicMock()
        mock_resp.json.return_value = data
        mock_get.return_value = mock_resp

        result = get_market_detail("will-btc-hit-100k")
        assert result["prices"]["yes_price"] == 0.0

    @patch("horizon.discovery.requests.get")
    def test_missing_tokens(self, mock_get):
        """Market with no tokens should still return structure."""
        data = _mock_market_detail_response()
        data[0]["tokens"] = []
        mock_resp = MagicMock()
        mock_resp.raise_for_status = MagicMock()
        mock_resp.json.return_value = data
        mock_get.return_value = mock_resp

        result = get_market_detail("will-btc-hit-100k")
        assert "market" in result
        # price_history should be empty since no token
        assert result["price_history"] == []

    @patch("horizon.discovery.requests.get")
    def test_api_failure_metadata(self, mock_get):
        """HTTP error during metadata fetch should raise."""
        import requests as _requests
        mock_resp = MagicMock()
        mock_resp.raise_for_status.side_effect = _requests.HTTPError("500")
        mock_get.return_value = mock_resp

        with pytest.raises(RuntimeError, match="Failed to fetch"):
            get_market_detail("will-btc-hit-100k")
